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

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。
  • 你当前配置的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.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.27 12:36:07