You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android 11 如何实现支持内外存储访问的File Picker文件选择器

Android 11 跨内部/外部存储文件选择器完整实现方案

Android 11 开始强制启用分区存储(Scoped Storage)策略,默认应用仅能访问自身私有目录和媒体库公共目录,要实现同时读取内部存储、可插拔SD卡等外部存储的全路径文件选择,可按以下步骤实现:

1. 清单权限声明

首先在 AndroidManifest.xml 中添加所需权限:

<!-- 适配Android 10及以下的基础存储读取权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<!-- Android 11及以上所需的全文件访问特殊权限 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- 适配Android 10及以下的存储写入权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

注意:MANAGE_EXTERNAL_STORAGE 属于敏感特殊权限,若需上架Google Play需提交应用核心功能依赖该权限的说明材料

2. 运行时权限申请

权限检查逻辑

fun checkStorageAccessPermission(context: Context): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        Environment.isExternalStorageManager()
    } else {
        ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
    }
}

权限申请逻辑

fun requestStorageAccessPermission(activity: Activity, requestCode: Int) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        // 跳转到系统设置页开启全文件访问权限
        val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
        intent.data = Uri.parse("package:${activity.packageName}")
        activity.startActivityForResult(intent, requestCode)
    } else {
        // 申请普通存储权限
        ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), requestCode)
    }
}

在页面的权限回调中确认权限获取成功后,再启动文件选择器页面。

3. 多存储路径枚举

要同时加载内部存储和外部可插拔存储的根路径,需要通过系统存储服务获取所有已挂载的存储设备:

fun getAllMountedStorageRoots(context: Context): List<File> {
    val storageRoots = mutableListOf<File>()
    // 先加入内置内部存储根路径
    val internalRoot = Environment.getExternalStorageDirectory()
    storageRoots.add(internalRoot)

    val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        val volumes = storageManager.storageVolumes
        volumes.forEach { volume ->
            // 筛选已挂载的可移除外部存储(SD卡、U盘等)
            if (volume.isRemovable && volume.state == Environment.MEDIA_MOUNTED) {
                volume.directory?.let { root ->
                    if (!storageRoots.contains(root)) storageRoots.add(root)
                }
            }
        }
    } else {
        // 适配Android 7以下版本,反射获取所有存储路径
        try {
            val getVolumePathsMethod = StorageManager::class.java.getMethod("getVolumePaths")
            val paths = getVolumePathsMethod.invoke(storageManager) as Array<String>
            paths.forEach { path ->
                val root = File(path)
                if (root.exists() && root.isDirectory && !storageRoots.contains(root)) {
                    storageRoots.add(root)
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    return storageRoots
}

4. 文件列表加载核心逻辑

  • 页面分为顶部存储切换栏、下方文件列表区域,可配置单选/多选、文件类型过滤规则
  • 目录遍历逻辑示例:
fun loadCurrentDirFileList(currentDir: File, fileFilter: (File) -> Boolean): List<File> {
    val resultList = mutableListOf<File>()
    if (!currentDir.exists() || !currentDir.isDirectory || !currentDir.canRead()) return resultList

    val allFiles = currentDir.listFiles() ?: return resultList
    val dirList = mutableListOf<File>()
    val fileList = mutableListOf<File>()

    allFiles.forEach { file ->
        // 过滤隐藏文件、无权限访问的文件
        if (file.name.startsWith(".") || !file.canRead()) return@forEach
        if (file.isDirectory) {
            dirList.add(file)
        } else {
            if (fileFilter.invoke(file)) fileList.add(file)
        }
    }

    // 按名称排序,文件夹优先展示
    dirList.sortBy { it.name.lowercase() }
    fileList.sortBy { it.name.lowercase() }
    resultList.addAll(dirList)
    resultList.addAll(fileList)
    return resultList
}

5. 选择结果处理

用户确认选择后直接返回文件的绝对路径即可,Android 11 下获取全文件权限后,可直接通过 File(absolutePath) 的方式对选中文件进行读写操作。
如果无需获取文件绝对路径,也可使用系统自带的Storage Access Framework(SAF)实现文件选择,不需要申请特殊权限,仅通过返回的Uri进行流操作即可


内容的提问来源于stack exchange,提问作者Haseeb Afzal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 05:57:01