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
RecoverableSecurityException
is not going to do you a lot of good in this case… becauseDESCRIPTION
cannot be modified, even if the user grants your app permission to modify the content. For some columns,MediaStore
just 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_PENDING
to1
-
Then, update the content to modify your desired bits of metadata and also set
IS_PENDING
back 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_PENDING
to1
- Makes a new
ContentValues
based on your passed-in one - Adds
IS_PENDING
as0
to that copy of theContentValues
- Updates the item with that augmented
ContentValues
copy - 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
DocumentFile
for 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
MediaStore
content from other apps - Limitations of
MediaStore.Downloads
- The undocumented
Documents
option - More on
RecoverableSecurityException
- How to modify more metadata in
MediaStore