在撰写屏幕之间传递 uri 会导致:SecurityException: Permission Denial
Passing uri between compose screens causes: SecurityException: Permission Denial
我通过以下方式在屏幕“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 repo 重现错误:https://github.com/geckogecko/PermissionDenialPlayground
发生在:Pixel 2 API 28、Pixel 4a API 31、Pixel 5 API 31 个模拟器
如果您查看 it.getString("imageUri")
的内容,您将看到以下内容:
content://com.android.providers.media.documents/document/image:20
如果您尝试使用以下代码 encode/decode 您的媒体 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 的推荐方法。
我通过以下方式在屏幕“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 repo 重现错误:https://github.com/geckogecko/PermissionDenialPlayground 发生在:Pixel 2 API 28、Pixel 4a API 31、Pixel 5 API 31 个模拟器
如果您查看 it.getString("imageUri")
的内容,您将看到以下内容:
content://com.android.providers.media.documents/document/image:20
如果您尝试使用以下代码 encode/decode 您的媒体 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 的推荐方法。