Scoped Storage Stories: Modifying the Content of Other Apps
Android 10 is greatly restricting access to external storage
via filesystem APIs. Instead, we need to use other APIs to work with content.
This is the tenth post in a series where we will explore how to work with those
alternatives, now looking at MediaStore options.
Earlier, we saw that you can store stuff via MediaStore
on Android 10 without WRITE_EXTERNAL_STORAGE as a permission. And, we saw
that you can read your own MediaStore stuff without a permission, but
you need READ_EXTERNAL_STORAGE to read other apps��� stuff
in the MediaStore. The remaining piece is: how do we modify other apps��� stuff
in the MediaStore?
You might think that WRITE_EXTERNAL_STORAGE is the solution. After all, if READ_EXTERNAL_STORAGE
lets you read other apps��� stuff, WRITE_EXTERNAL_STORAGE should let you
modify other apps��� stuff. Right?
(if you have not read much of my writing, when I use that sort of ���leading argument���
paragraph structure, it is almost always creative foreshadowing of a reversal)
(we return you now to your regularly-scheduled blog post, already in progress���)
In truth, WRITE_EXTERNAL_STORAGE does not help here. WRITE_EXTERNAL_STORAGE grants
blanket access, and one of the objectives of Android 10���s scoped storage is to get
rid of that sort of blanket access.
Instead, Android 10 introduces the concept of a RecoverableSecurityException.
If we catch one of these, the exception allows us to request fine-grained access
from the user, and we can use that to proceed.
To illustrate this, let���s look at Google���s storage-samples repo
and the MediaStore sample inside of it.
This module has a MainActivityViewModel that, among other things, offers a performDeleteImage()
function. Basically, given a Uri, it will attempt to delete the item from the
MediaStore. However, frequently the item was not created by the app, but instead
was found by querying the MediaStore. As a result, deletion often fails��� but
we can recover from it.
private suspend fun performDeleteImage(image: MediaStoreImage) {
withContext(Dispatchers.IO) {
try {
/**
* In [Build.VERSION_CODES.Q] and above, it isn't possible to modify
* or delete items in MediaStore directly, and explicit permission
* must usually be obtained to do this.
*
* The way it works is the OS will throw a [RecoverableSecurityException],
* which we can catch here. Inside there's an [IntentSender] which the
* activity can use to prompt the user to grant permission to the item
* so it can be either updated or deleted.
*/
getApplication<Application>().contentResolver.delete(
image.contentUri,
"${MediaStore.Images.Media._ID} = ?",
arrayOf(image.id.toString())
)
} catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException
?: throw securityException
// Signal to the Activity that it needs to request permission and
// try the delete again if it succeeds.
pendingDeleteImage = image
_permissionNeededForDelete.postValue(
recoverableSecurityException.userAction.actionIntent.intentSender
)
} else {
throw securityException
}
}
}
}
This function tries to delete() the content using a ContentResolver.
If delete() throws an exception, we can see if that exception is
a RecoverableSecurityException. If it is, rather than treat the exception
normally, we use userAction.actionIntent.intentSender to get an IntentSender
associated with the exception. IntentSender is an uncommon class, but you get
one from a PendingIntent via getIntentSender(). Like a PendingIntent,
an IntentSender has the ability to send an Intent, where the holder of the
IntentSender does not know what the action is (start an activity? send a broadcast?
start a service?) or who the recipient is.
MainActivityViewModel then posts that IntentSender to a LiveData, which is
observed by MainActivity:
viewModel.permissionNeededForDelete.observe(this, Observer { intentSender ->
intentSender?.let {
// On Android 10+, if the app doesn't have permission to modify
// or delete an item, it returns an `IntentSender` that we can
// use here to prompt the user to grant permission to delete (or modify)
// the image.
startIntentSenderForResult(
intentSender,
DELETE_PERMISSION_REQUEST,
null,
0,
0,
0,
null
)
}
})
Here, we use startIntentSenderForResult() on Activity to invoke the IntentSender.
If the IntentSender wraps an activity Intent, startIntentSenderForResult()
will call startActivityForResult() on that underlying Intent. In the case
of the IntentSender from the RecoverableSecurityException, this will bring up a UI to
allow the user to grant your app rights to modify the content affected by
the failed delete() request. And, if onActivityResult() gets RESULT_OK
for the startIntentSenderForResult() call, this means the user granted you
permission, and you can retry your delete(), which should now succeed.
Many thanks to Nicole Borelli of Google for creating and posting this sample code!
Previously, I covered:
The basics of using the Storage Access Framework
Getting durable access to the selected content
Working with DocumentFile for individual documents
Working with document trees
Working with DocumentsContract
Problems with the SAF API
A specific problem with listFiles() on DocumentFile
Storing content using MediaStore
Reading content from the MediaStore
In the next MediaStore post, I will look at the new Downloads category of
���media��� and how we can use that in Android 10+.


