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
相关产品推荐
相关产品推荐

