在组合屏幕之间传递URI导致:SecurityException:Permission Denial。

5

我在屏幕“A”中通过以下方式接收uri:

val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
        if(activityResult.resultCode == Activity.RESULT_OK) {
            val uri = activityResult.data?.data!!
            context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
            viewModel.onUriReceived(uri)
        }
    }
LaunchedEffect(launcher) {
     val intent = Intent(Intent.ACTION_OPEN_DOCUMENT, MediaStore.Video.Media.EXTERNAL_CONTENT_URI).apply {
         addCategory(Intent.CATEGORY_OPENABLE)
         type = "video/*"
         addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
         addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
     }
     coroutineScope.launch {
         launcher.launch(intent)
     }
  }

我可以在屏幕“A”中打开URI,但如果我将此URI传递给屏幕“B”,则会收到以下信息:
 java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaDocumentsProvider uri content://com.android.providers.media.documents/document/video:38 from pid=6074, uid=10146 requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
        at android.os.Parcel.createExceptionOrNull(Parcel.java:2425)
        at android.os.Parcel.createException(Parcel.java:2409)
        at android.os.Parcel.readException(Parcel.java:2392)
        at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:190)
        at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:153)
        at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:780)
        at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:2027)
        at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1842)
        at android.content.ContentResolver.openInputStream(ContentResolver.java:1518)

复现错误的Github仓库: https://github.com/geckogecko/PermissionDenialPlayground 出现于: Pixel 2 API 28、Pixel 4a API 31和Pixel 5 API 31模拟器等设备上

1个回答

6
如果您检查it.getString("imageUri")的内容,您将看到以下内容:
content://com.android.providers.media.documents/document/image:20

如果您尝试使用以下代码对媒体URI进行编码/解码:

val encoded = Uri.encode(uri.toString())
val decoded = Uri.decode(encoded)
val decoded2 = Uri.decode(decoded)
println("original: $uri")
println(" encoded: $encoded")
println(" decoded: $decoded")
println("decodedSecond: $decodedSecond")

你应该看到这个:
original: content://com.android.providers.media.documents/document/image%3A20
 encoded: content%3A%2F%2Fcom.android.providers.media.documents%2Fdocument%2Fimage%253A20
 decoded: content://com.android.providers.media.documents/document/image%3A20
decoded2: content://com.android.providers.media.documents/document/image:20

Android导航会自行解码参数,并且看起来它会进行多次解码,因此您会看到导航参数等于结果decoded2而不是decoded

原因是原始URI包含编码符号'%',因此在第二次解码阶段后,'%3A'变成了':'

一个可能的解决方案是为该符号添加自己的额外“编码”。我选择了'|',因为我认为它在android系统提供给您的任何URI中都不会被表示,但如果您有问题,可以想出另一个字符。

val encoded = Uri.encode(uri.toString().replace('%','|'))
navController.navigate("screenB?imageUri=$encoded")

解码:

val uri = it.arguments?.let {
    it.getString("imageUri")
        ?.replace('|','%')
        ?.let(Uri::parse)
}

另一种选择是不对其进行编码,而是将其存储在存储库内的某个容器中,在路由之间共享 - 可以是单例或Hilt DI等。因此,您将容器ID作为参数传递,通常这是Compose Navigation的推荐方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接