Scoped Storage Stories: MediaStore Metadata Madness
Android 10 and higher are greatly restricting access to external storage via filesystem APIs. Instead, we need to use other APIs to work with content. This is the 14th post in a seemingly never-ending series, where we will explore how to work with those alternatives.
Previously, I wrote about modifying the content of other apps
and responding to a RecoverableSecurityException.
In that latter post, I wrote:
Unfortunately, that
RecoverableSecurityExceptionis not going to do you a lot of good in this case… becauseDESCRIPTIONcannot be modified, even if the user grants your app permission to modify the content. For some columns,MediaStorejust refuses to apply the update.
It turns out that there is a recipe for making this work, though the approach is bizarre:
-
First, update the content to set
IS_PENDINGto1 -
Then, update the content to modify your desired bits of metadata and also set
IS_PENDINGback to0
IS_PENDING was added in Android 10. The idea is that you set IS_PENDING to 1
to indicate that you are setting up the content, such as copying data to the Uri
supplied by the MediaStore. Apps, when querying the MediaStore, can opt out of
seeing pending items, as they may not be useful yet. Later, you flip IS_PENDING
to 0, to indicate that the content is ready for use.
Inexplicably, in an update() call on ContentResolver for MediaStore, where
you flip IS_PENDING from 1 to 0, you can also update other metadata columns
like DESCRIPTION. If IS_PENDING is not already 1, this does not work, so you
wind up having to set IS_PENDING to 1 first, then set it to 0 while updating
the rest of your metadata.
I’m sure that this system makes sense to somebody… but not me. However, it works
on Android 10. I am still running into problems in general with MediaStore
and RecoverableSecurityException on R DP2, so I do not know if anything with
this changes there.
The v2 tagged version of my MovieLister sample
demonstrates this approach. VideoRepository uses a setMetadata() function
to update metadata using this IS_PENDING hack:
private suspend fun setMetadata(id: Long, values: ContentValues) =
withContext(Dispatchers.IO) {
val uri = ContentUris.withAppendedId(collection, id)
if (Build.VERSION.SDK_INT >= 29) {
val tempValues = ContentValues().apply {
put(MediaStore.Video.Media.IS_PENDING, 1)
}
context.contentResolver.update(uri, tempValues, null, null)
}
val updatedValues =
if (Build.VERSION.SDK_INT >= 29) {
ContentValues(values).apply {
put(MediaStore.Video.Media.IS_PENDING, 0)
}
} else {
values
}
context.contentResolver.update(uri, updatedValues, null, null)
}
You pass in the ContentValues that you wish to apply, and setMetadata():
- Sets
IS_PENDINGto1 - Makes a new
ContentValuesbased on your passed-in one - Adds
IS_PENDINGas0to that copy of theContentValues - Updates the item with that augmented
ContentValuescopy - Only goes through all that crap on Android 10 and higher, skipping it for older devices
To me, this approach screams “unintended side effect”, and I would not be surprised
if it breaks in Android R or future versions. Also, bear in mind that some of these
metadata values may get clobbered if the MediaStore re-indexes the content. update()
is updating the MediaStore, not metadata that might be in the media itself (e.g., EXIF
tags in a JPEG).
But, at least for Android 10, it is an option that you can consider.
UPDATE 2020-04-11: Stack Overflow user CodeRed pointed out
that you can see what values are mutable without IS_PENDING by examining the
sMutableColumns field in MediaProvider. In Android 10, this looks like:
private static final ArraySet<String> sMutableColumns = new ArraySet<>();
{
sMutableColumns.add(MediaStore.MediaColumns.DATA);
sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
sMutableColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
sMutableColumns.add(MediaStore.MediaColumns.PRIMARY_DIRECTORY);
sMutableColumns.add(MediaStore.MediaColumns.SECONDARY_DIRECTORY);
sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS);
sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY);
sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK);
sMutableColumns.add(MediaStore.Audio.Playlists.NAME);
sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID);
sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE);
sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE);
}
Anything else — like DESCRIPTION — requires modifying it after
going through the IS_PENDING hop described in this post.
The entire series of “Scoped Storage Stories” posts includes posts on:
- The basics of using the Storage Access Framework
- Getting durable access to the selected content
- Working with
DocumentFilefor individual documents - Working with document trees
- Working with
DocumentsContract - Problems with the SAF API
- A specific problem with
listFiles()onDocumentFile - Storing content using
MediaStore - Reading content from the
MediaStore - Modifying
MediaStorecontent from other apps - Limitations of
MediaStore.Downloads - The undocumented
Documentsoption - More on
RecoverableSecurityException - How to modify more metadata in
MediaStore

