Batched Access

Earlier, we saw how in Android 10 we could catch a RecoverableSecurityException and use that to get permission to modify a single piece of content. For many apps, this is sufficient, because they only need to modify one piece of content at a time. However, for apps that manipulate multiple pieces of content, this per-piece-permission approach is awful. The user winds up having to accept a dialog for each piece, and that gets tedious quickly.

Android 11 offers a batched way to get permission from the user, though the API for it is rather odd.

The VideoTagger sample module in the book’s sample project requests READ_EXTERNAL_STORAGE permission on startup and uses that to query the MediaStore for all the videos on external storage. Those are then presented in a checklist:

VideoTagger, As Initially Launched
VideoTagger, As Initially Launched

The user can check one or more of the videos, then fill in “tags” in the field and click “Set Tags”. This will attempt to update the TAGS property of the content… and that will fail initially. The app then requests write permission for all of those videos at once:

VideoTagger, with Permission Request Dialog
VideoTagger, with Permission Request Dialog

If the user grants the permission, the app updates the TAGS and reloads the list, showing those tags below the video title. If the user leaves the field blank and clicks “Set Tags”, the same thing happens, except that any existing TAGS value is simply cleared.

Obtaining the Image Uri Values

There are many ways we could have gotten Uri values for the user’s chosen set of videos. For example, we could have used ACTION_OPEN_DOCUMENT, ACTION_GET_CONTENT, or ACTION_PICK. In particular, ACTION_OPEN_DOCUMENT and ACTION_GET_CONTENT should support EXTRA_ALLOW_MULTIPLE, so we can request a UI that allows the user to pick several items at once.

However, the Uri values that we get back from those do not work with this batched permission request feature. If we try, we get:

java.lang.IllegalArgumentException: Missing volume name: content://com.android.providers.media.documents/document/video%3A23

(for whatever Uri you happened to try)

For ACTION_OPEN_DOCUMENT and ACTION_GET_CONTENT, we get “document Uri” values. You might think that the solution is to convert those to “media Uri” values, using getMediaUri() on MediaStore. While we can call that method and convert the Uri values, the converted values also do not work, as we get:

java.lang.IllegalStateException: java.io.FileNotFoundException: No root for video

(at least for a video Uri — the error probably varies based on media type)

The way that works is the one described earlier in this chapter: use getContentUri().

That is what the VideoTagger app uses. We have a VideoModel that represents the data that we need for a video:

package com.commonsware.android.r.videotagger

import android.net.Uri

data class VideoModel(
  val uri: Uri,
  val title: String,
  val tags: String?,
  val description: String?,
  var isChecked: Boolean = false
)

When our VideoRepository queries the MediaStore and converts the Cursor to a List of VideoModel, it uses getContentUri() to fill in the uri property:

  private val resolver = context.contentResolver

  suspend fun loadVideos(): List<VideoModel> =
    withContext(Dispatchers.IO) {
      val collection =
        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)

      resolver.query(collection, PROJECTION, null, null, SORT_ORDER)
        ?.use { cursor ->
          cursor.mapToList {
            VideoModel(
              uri = MediaStore.Video.Media.getContentUri(
                MediaStore.VOLUME_EXTERNAL,
                it.getLong(0)
              ),
              title = it.getString(1),
              tags = it.getString(2),
              description = it.getString(3)
            )
          }
        } ?: emptyList()
    }

Seeing If We Have Permission

Once the user has selected some videos, if the user clicks the “Set Tags” button, we need to:

All of this is a bit clunky.

To see if we have permission to modify the content identified by a Uri, we need to call checkUriPermission() on a Context. This takes four parameters:

This will return PackageManager.PERMISSION_GRANTED if your app holds that particular permission.

MainActivity in VideoTagger has a neededPermissions() function that takes the list of videos and returns the subset for which we still need to obtain permission from the user:

  private fun neededPermissions(selections: List<Uri>) =
    selections.filter {
      checkUriPermission(
        it,
        Process.myPid(),
        Process.myUid(),
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
      ) != PackageManager.PERMISSION_GRANTED
    }

Requesting the Permission

To get write permission for those Uri values where we lack it, we need to do two things.

First, we need to call MediaStore.createWriteRequest(), supplying the list of Uri values and a ContentResolver. This returns a PendingIntent that represents this request for write access.

Then, we need to call startIntentSenderForResult(). If the PendingIntent is one for an activity (PendingIntent.getActivity()), then startIntentSenderForResult() will start that activity and send any result back to our onActivityResult() function. This works just as it would if we called startActivityForResult() on a regular Intent.

When the user clicks “Set Tags”, that triggers a call to an applyTags() function. This uses neededPermissions() to find the Uri values for which we lack write access. If we have write access to all of them, we go ahead and ask MainMotor to update the tags. Otherwise, we call MediaStore.createWriteRequest() and startIntentSenderForResult() to request permission from the user:

  private fun applyTags(models: List<VideoModel>) {
    val needed = neededPermissions(models.map { it.uri })

    if (needed.isEmpty()) {
      motor.applyTags(models, binding.tags.text.toString())
    } else {
      val pi = MediaStore.createWriteRequest(contentResolver, needed)

      startIntentSenderForResult(pi.intentSender, REQUEST_PERMS, null, 0, 0, 0)
    }
  }

This is what triggers the dialog shown earlier. In our onActivityResult() function, if our REQUEST_PERMS request succeeded, we can go ahead and try applyTags() again.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.