Android 11 已配置存储权限但文件选择器无法读取本地非媒体文件
问题根因定位
- 非媒体文件读取失败的核心原因,是在Android 11分区存储机制下,仍然沿用老旧的“将Content URI硬解码为物理文件路径”的实现逻辑(也就是你代码里用到的
pickiT.getPath()这类第三方路径转换库)。 - 对比可正常/异常的URI差异,就能明确问题触发点:
- 可正常读取的媒体文件返回URI:
content://com.android.providers.downloads.documents/document/103,这类纯数字ID的URI,老转换库可以通过查询Downloads系统数据库拿到真实文件绝对路径,且媒体文件在分区存储下有公开读权限,因此可以正常访问。 - 无法读取的非媒体PDF返回URI:
content://com.android.providers.downloads.documents/document/msf:1390,这类带msf:前缀的URI是Android 11存储访问框架(SAF)返回的封装URI,根本不存在可直接访问的对应物理文件路径。老转换库遇到这类ID时,只会强行拼接出/storage/emulated/0/Download/xxx.pdf的无效路径,应用对这个拼接出的路径没有任何读权限,自然会触发EACCES报错,且canRead()返回false。
- 可正常读取的媒体文件返回URI:
- 你当前配置的FileProvider路径、存储读取权限,在这个场景下完全不生效:FileProvider是用于向其他应用共享自身授权目录下文件的组件,和读取第三方SAF返回的URI没有关联。
修复方案
核心逻辑调整原则
彻底废弃所有“将Content URI转换为本地绝对路径”的实现,全程通过系统授予的临时URI权限操作文件。
从Android 10开始,应用不再拥有公共目录的无限制读权限,通过SAF拿到的URI,系统会直接给应用绑定该URI的临时读权限,这个权限仅和URI本身绑定,和文件的物理路径完全无关。
具体落地步骤
- 替换ActivityResult回调逻辑,移除
pickiT.getPath()的路径转换代码。直接通过ContentResolver打开URI的输入流,所有需要读文件的操作(计算MD5、内容上传、文件解析)都基于输入流完成,不需要获取文件绝对路径:override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == SELECT_FILE_REQUEST && resultCode == RESULT_OK) { data?.data?.let { uri -> // 直接通过内容解析器获取临时授权的输入流,无需要转换为本地文件路径 contentResolver.openInputStream(uri)?.use { inputStream -> // 在此完成业务逻辑:比如计算MD5、读取文件内容、上传云端 val fileMd5 = calculateMd5FromStream(inputStream) } // 需要文件元数据(文件名、大小)时,通过URI查询获取,不需要拿真实路径 val fileName = queryFileNameFromUri(uri, contentResolver) } } super.onActivityResult(requestCode, resultCode, data) } - 如果业务依赖的第三方SDK必须传入本地文件路径(不支持流操作),则将从URI读取的流内容,复制到应用自身的私有缓存目录下。操作私有缓存目录下的文件完全不受分区存储限制,不需要额外申请任何权限:
fun copyUriContentToPrivateCache(uri: Uri, contentResolver: ContentResolver): File? { val targetFileName = queryFileNameFromUri(uri, contentResolver) ?: return null val targetFile = File(cacheDir, targetFileName) contentResolver.openInputStream(uri)?.use { input -> FileOutputStream(targetFile).use { output -> input.copyTo(output) } } return targetFile } - 优化文件选择Intent的实现,将原来的
ACTION_GET_CONTENT替换为ACTION_OPEN_DOCUMENT,避免不同系统实现导致的URI临时权限丢失问题:fun pickFile(fragment: Fragment) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) val mimeTypes = arrayOf( "image/*", "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) intent.type = "*/*" intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) fragment.startActivityForResult(intent, SELECT_FILE_REQUEST) }
误区提醒
- 不需要为这个场景申请
MANAGE_EXTERNAL_STORAGE全量存储权限,该权限目前应用市场审核要求极高,完全没必要通过临时URI授权的方式已经可以覆盖所有文件读取需求。 - 之前配置的FileProvider路径不需要删除,该配置适用于应用自身对外共享文件(比如调起系统相机、向其他应用分享文件)的场景,和当前读取SAF文件的问题无关。
内容的提问来源于stack exchange,提问作者Anton A.
相关产品推荐
相关产品推荐

