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:
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:
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:
- See if we have write permission for all of those videos
- Request write permission for those that we lack
- Update the tags once we get write permissions for the videos
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:
- The
Uri
to check - Your process ID (
Process.myPid()
) - Your app’s user ID (
Process.myUid()
) - The permission to check (e.g.,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
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.