'Passing uri between compose screens causes: SecurityException: Permission Denial

I receive a uri in screen "A" by:

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)
     }
  }

I can open the uri in screen "A" but if I pass the uri to screen "B" I receive:

 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 to reproduce the error: https://github.com/geckogecko/PermissionDenialPlayground Happened with: Pixel 2 API 28, Pixel 4a API 31, Pixel 5 API 31 emulators



Solution 1:[1]

If you check the content of it.getString("imageUri"), you'll see the following:

content://com.android.providers.media.documents/document/image:20

And if you try to encode/decode your media uri with the following code:

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")

You should see this:

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 navigation decodes the arguments on its own, and it looks like it does it multiple times, since you see that the navigation argument equals the result decoded2 rather than decoded.

The reason is that original uri contains encoding symbol '%', so '%3A' becomes ':' after the second decoding phase.

A possible solution is to add your own additional "encoding" for this symbol. I chose '|' because I expect it will not be represented in any uri that the android system will give you, but you can come up with another character if you have problems.

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

Decode:

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

Another option is not to encode it, but to store it in some container inside a repository, shared between routes in some way - it may be singleton or Hilt DI, etc. So you pass the container id as an argument, in general this is the recommended approach for Compose Navigation.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Pylyp Dukhov