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

Jetpack Compose中takePersistableUriPermission持久化URI权限失效问题

问题:使用ACTION_OPEN_DOCUMENT_TREE时,已授权文件夹仍重复请求权限

我正在尝试用ACTION_OPEN_DOCUMENT_TREE将文件保存到外部存储,功能本身正常,但每次用户选择文件夹时都会弹出权限请求。我希望用户能随时选择任意文件夹,但对已授权过的文件夹不再重复请求权限。

我的代码

选择文件夹的Launcher代码

val saveToDevicePickFolderLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
        uri?.let {
            try {
                App.instance.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                Logger.i { "Permissions persisted for URI: $it" }
            } catch (e: SecurityException) {
                Logger.e { "Failed to take persistable URI permission: ${e.localizedMessage}" }
            }
            viewModel.saveToDevice(selectedFileList, it, context)
            toggleSelectMode(on = false)
        }
    }

启动Launcher的调试代码

saveFileListToDevice = { list ->
   
    val persistedUriPermission = App.instance.contentResolver.persistedUriPermissions.firstOrNull()
    val persistedUri: Uri? = persistedUriPermission?.uri
    Logger.i { "Retrieved persisted URI $persistedUri with read? ${persistedUriPermission?.isReadPermission} and write? ${persistedUriPermission?.isWritePermission} permissions" }

    if (persistedUri != null) {
        try {
            // Ensure persistable URI permissions are granted for the persisted URI
            App.instance.contentResolver.takePersistableUriPermission(
                persistedUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            )
            Logger.i { "Permissions persisted for URI: $persistedUri" }
        } catch (e: SecurityException) {
            Logger.i { "Failed to take persistable URI permission: ${e.localizedMessage}" }
        }

        // Check if external storage is mounted and accessible
        when (val storageState = Environment.getExternalStorageState(File(persistedUri.path))) {
            Environment.MEDIA_MOUNTED -> {
                Logger.i { "External storage is mounted and writable." }
            }
            Environment.MEDIA_MOUNTED_READ_ONLY -> {
                Logger.i { "External storage is mounted, but read-only." }
            }
            Environment.MEDIA_UNMOUNTED, Environment.MEDIA_REMOVED -> {
                Logger.i { "External storage is not available." }
            }
            else -> {
                Logger.i { "Unknown state for external storage: $storageState" }
            }
        }

        val documentFile = DocumentFile.fromTreeUri(context, persistedUri)
        if (documentFile != null && documentFile.exists()) {
            // The URI is valid and the document exists
            Logger.i { "Document exists or URI is VALID" }
            saveToDevicePickFolderLauncher.launch(persistedUri)
        } else {
            Logger.i { "Document does not exist or URI is invalid" }
            saveToDevicePickFolderLauncher.launch(null)
        }
    } else {
        Logger.i { "Opening for input null" }
        saveToDevicePickFolderLauncher.launch(null)
    }
},

日志信息

Permissions persisted for URI: content://com.android.externalstorage.documents/tree/primary%3ADownload%2FA
Retrieved persisted URI UriPermission {uri=content://com.android.externalstorage.documents/tree/primary%3ADownload%2FA, modeFlags=3, persistedTime=1732635607603} with read? true and write? true permissions
Permissions persisted for URI: content://com.android.externalstorage.documents/tree/primary%3ADownload%2FA
Unknown state for external storage: unknown
Document exists or URI is VALID
Permissions persisted for URI: content://com.android.externalstorage.documents/tree/primary%3ADownload%2FA

已完成的检查

  • 确认在选择和访问文件夹时均正确调用了takePersistableUriPermission() ✅
  • 通过contentResolver.persistedUriPermissions验证已授予读写权限 ✅
  • 通过DocumentFile.fromTreeUri()确认URI有效 ✅
  • 检查外部存储状态时返回Unknown ❌,这可能是问题原因,但我不知道该如何处理?

解决方案

核心问题:误用Launcher启动逻辑

当你调用saveToDevicePickFolderLauncher.launch(persistedUri)时,即便URI已有持久化权限,ACTION_OPEN_DOCUMENT_TREE的默认行为仍会打开文件选择器让用户确认,这就是重复弹出权限请求的原因。持久化权限的作用是让你无需再申请权限就能直接操作该URI对应的文件夹,而非跳过文件选择器。

正确逻辑调整

  1. 若存在已授权URI,直接用该URI执行保存操作,无需启动选择器。
  2. 仅当用户需要更换文件夹时,再启动选择器让用户重新选择。

修改后的代码示例

调整saveFileListToDevice的逻辑:

saveFileListToDevice = { list ->
    val persistedUriPermission = App.instance.contentResolver.persistedUriPermissions.firstOrNull()
    val persistedUri: Uri? = persistedUriPermission?.uri

    if (persistedUri != null) {
        val documentFile = DocumentFile.fromTreeUri(context, persistedUri)
        if (documentFile != null && documentFile.exists()) {
            // 直接使用已授权URI保存,跳过选择器
            viewModel.saveToDevice(selectedFileList, persistedUri, context)
            toggleSelectMode(on = false)
        } else {
            // URI失效,重新启动选择器
            saveToDevicePickFolderLauncher.launch(null)
        }
    } else {
        // 无已授权URI,启动选择器
        saveToDevicePickFolderLauncher.launch(null)
    }
},

关于外部存储状态返回Unknown的问题

Environment.getExternalStorageState(File(persistedUri.path))不适用于SAF的URI,因为SAF URI对应的是文档树,而非传统文件路径,persistedUri.path无法获取真实文件系统路径,导致检测失败。直接删除这部分代码即可,DocumentFile.fromTreeUri()已能验证URI有效性。

额外建议

  • 若需支持多常用文件夹,不要仅取persistedUriPermissions.firstOrNull(),可将用户选择过的URI存储到SharedPreferences中,提供切换入口。
  • 监听URI失效场景:比如用户删除了授权文件夹,此时DocumentFile.exists()返回false,需重新启动选择器让用户重新选择。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 22:47:12