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

Android API21+获取Documents目录下PDF文件Uri的兼容方案

兼容API21+获取公共Documents目录PDF文件的Uri用于通知跳转

核心思路

Android 10(API29)及以上启用分区存储后,file:// Uri会触发FileUriExposedException,公共Documents目录的文件需通过MediaStore查询或DownloadManager直接获取content://类型的Uri,才能安全地在跨应用场景(如通知跳转打开PDF)中使用。

方案1:通过DownloadManager下载ID获取Uri(最可靠)

如果保存了DownloadManager返回的下载ID,直接查询该ID对应的文件Uri,避免文件名匹配错误:

fun getDownloadedPdfUri(context: Context, downloadId: Long): Uri? {
    val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
    val query = DownloadManager.Query().setFilterById(downloadId)
    
    downloadManager.query(query)?.use { cursor ->
        if (cursor.moveToFirst()) {
            val uriColumnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
            return Uri.parse(cursor.getString(uriColumnIndex))
        }
    }
    return null
}

方案2:通过MediaStore查询文件名获取Uri

如果没有保存下载ID,可通过MediaStore索引公共Documents目录的文件,匹配文件名和MIME类型获取Uri:

fun getPdfUriByFileName(context: Context, fileName: String): Uri? {
    val projection = arrayOf(
        MediaStore.Files.FileColumns._ID,
        MediaStore.Files.FileColumns.DISPLAY_NAME
    )
    
    // 匹配文件名和PDF类型
    val selection = """
        ${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? 
        AND ${MediaStore.Files.FileColumns.MIME_TYPE} = ?
    """.trimIndent()
    val selectionArgs = arrayOf(fileName, "application/pdf")
    
    val contentUri = MediaStore.Files.getContentUri("external")
    
    context.contentResolver.query(
        contentUri,
        projection,
        selection,
        selectionArgs,
        null
    )?.use { cursor ->
        if (cursor.moveToFirst()) {
            val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
            return ContentUris.withAppendedId(contentUri, id)
        }
    }
    return null
}

通知跳转Intent的关键配置

确保Intent正确授予临时读取权限,适配API33:

val pdfUri = getDownloadedPdfUri(context, yourDownloadId) ?: return

val flags = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
    PendingIntent.FLAG_UPDATE_CURRENT
} else {
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
}

val viewIntent = Intent(Intent.ACTION_VIEW).apply {
    setDataAndType(pdfUri, "application/pdf")
    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    // API30+ 可选添加,确保权限覆盖Uri的前缀路径
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
    }
}

// 创建PendingIntent并设置到通知
setContentIntent(
    PendingIntent.getActivity(
        context,
        1,
        viewIntent,
        flags
    )
)

权限适配(Target SDK33)

  • API23~32:需要在Manifest中声明READ_EXTERNAL_STORAGE权限,并动态申请
  • API33:对于PDF这类非媒体文件,仍需声明READ_EXTERNAL_STORAGE权限并动态申请(READ_MEDIA_*权限仅针对图片、视频、音频)

Manifest声明:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="33" />

为什么之前的方法失败?

  • FileProvider:默认配置仅覆盖应用私有目录,若要适配公共Documents目录,需额外在file_paths.xml中添加<external-path name="documents" path="Documents/" />,但该方式依赖文件路径的稳定性,不如MediaStore或DownloadManager查询可靠。
  • 直接File转Uri:API29+会触发FileUriExposedException,属于不安全的跨应用文件访问方式。

内容的提问来源于stack exchange,提问作者Gabriel Melo Cavalcante

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 14:28:22