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

Android 10下ContentResolver无法获取其他应用添加文件的解决方案问询

问题根源分析

在Android 10(API 29)的分区存储机制下,出现这个问题的核心原因有两点:

  • 通过ContentResolver.insert添加的文件会直接写入MediaStore数据库,所以能被你的查询立即捕获;
  • 第三方应用(如文件管理器)直接写入的文件,默认不会自动触发MediaStore的索引更新,再加上xls/xlsx属于非媒体文件,系统也不会主动扫描收录,因此这些文件不会出现在MediaStore的查询结果中。

可行解决方案

方案1:使用SAF(存储访问框架)授权并遍历目录

这是最可靠的方法,完全不受MediaStore索引限制,能直接获取目录下所有文件(无论是否被MediaStore收录)。

步骤说明

  1. 引导用户授权访问Documents/MyExcelsFolder目录;
  2. 持久化授权权限,避免重复请求;
  3. 通过SAF API遍历目录下的Excel文件。

代码示例

首先,启动SAF选择目录:

private const val REQUEST_CODE_OPEN_DIR = 1001

// 在Activity/Fragment中调用,引导用户选择目标目录
fun requestFolderAccess() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
    // 可选:直接引导到Documents目录,减少用户操作
    val documentsUri = MediaStore.Files.getContentUri("external").buildUpon()
        .appendPath(Environment.DIRECTORY_DOCUMENTS)
        .build()
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentsUri)
    startActivityForResult(intent, REQUEST_CODE_OPEN_DIR)
}

然后处理授权结果:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_CODE_OPEN_DIR && resultCode == Activity.RESULT_OK) {
        data?.data?.let { treeUri ->
            // 持久化读取权限,下次启动无需重新授权
            requireContext().contentResolver.takePersistableUriPermission(
                treeUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION
            )
            // 遍历目录下的所有Excel文件
            val excelFiles = listExcelFilesInTree(requireContext(), treeUri)
            // 后续处理文件列表逻辑
        }
    }
}

实现目录遍历方法:

fun listExcelFilesInTree(context: Context, treeUri: Uri): List<AppFile> {
    val excelFiles = mutableListOf<AppFile>()
    val allowedExtensions = setOf("xls", "xlsx")
    val allowedMimes = setOf(
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )

    // 构建子文档查询Uri
    val childDocumentsUri = DocumentsContract.buildChildDocumentsUriUsingTree(
        treeUri,
        DocumentsContract.getTreeDocumentId(treeUri)
    )

    context.contentResolver.query(
        childDocumentsUri,
        arrayOf(
            DocumentsContract.Document.COLUMN_DOCUMENT_ID,
            DocumentsContract.Document.COLUMN_DISPLAY_NAME,
            DocumentsContract.Document.COLUMN_MIME_TYPE,
            DocumentsContract.Document.COLUMN_LAST_MODIFIED
        ),
        null,
        null,
        "${DocumentsContract.Document.COLUMN_LAST_MODIFIED} DESC"
    )?.use { cursor ->
        val docIdIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
        val nameIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
        val mimeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)

        while (cursor.moveToNext()) {
            val docId = cursor.getString(docIdIndex)
            val fileName = cursor.getString(nameIndex)
            val mimeType = cursor.getString(mimeIndex)

            // 判断是否为Excel文件
            val isExcel = allowedMimes.contains(mimeType) ||
                    allowedExtensions.contains(fileName.substringAfterLast('.', "").lowercase())

            if (isExcel) {
                val fileUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, docId)
                excelFiles.add(
                    AppFile(
                        id = docId.hashCode().toLong(), // 用docId生成唯一标识
                        uri = fileUri,
                        absolutePath = null, // Android 10下无法直接获取绝对路径
                        name = fileName,
                        mimeType = mimeType ?: getMimeTypeFromName(fileName),
                        extension = fileName.substringAfterLast('.', ""),
                        relativePath = "Documents/MyExcelsFolder/"
                    )
                )
            }
        }
    }
    return excelFiles
}

private fun getMimeTypeFromName(fileName: String): String {
    val extension = fileName.substringAfterLast('.', "").lowercase()
    return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "application/octet-stream"
}

方案2:触发MediaStore扫描目标文件

如果能预知新文件的具体路径,可以调用MediaScannerConnection扫描文件,让系统将其收录到MediaStore中,这样你的原查询方法就能获取到。

代码示例

fun scanFile(context: Context, filePath: String) {
    MediaScannerConnection.scanFile(
        context,
        arrayOf(filePath),
        arrayOf(getMimeTypeFromName(filePath))
    ) { scannedPath, uri ->
        // 扫描完成后,重新查询文件列表
        val files = getAppFiles(context)
    }
}

局限性:需要知道具体文件路径才能扫描,无法自动捕获第三方应用添加的所有文件。

方案3:优化MediaStore查询条件

确保你的查询条件能准确匹配目标目录,覆盖RELATIVE_PATH带斜杠和不带斜杠的情况:

val selection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    // 匹配RELATIVE_PATH为"Documents/MyExcelsFolder/"或"Documents/MyExcelsFolder"
    "${MediaStore.MediaColumns.RELATIVE_PATH} = ? OR ${MediaStore.MediaColumns.RELATIVE_PATH} = ?"
} else {
    "${MediaStore.Images.Media.DATA} LIKE ?"
}
val selectionArgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    arrayOf("Documents/MyExcelsFolder/", "Documents/MyExcelsFolder")
} else {
    arrayOf("%${Environment.DIRECTORY_DOCUMENTS}/MyExcelsFolder%")
}

注意:这个方法仅能解决查询条件的准确性问题,无法解决MediaStore未索引第三方文件的核心问题。

关键注意事项

  • Android 10下无法直接通过File API访问外部存储的非私有目录,必须依赖SAF或MediaStore;
  • 使用SAF时,无法获取文件的绝对路径,只能通过Uri来访问文件内容,因此你的AppFile类的absolutePath字段需要改为可空类型,或者调整业务逻辑为基于Uri操作文件。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 15:07:55