Scoped Storage Stories: Reading via MediaStore
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 ninth post in a series where we will explore how to work with those
alternatives, now looking at MediaStore
options.
In our last episode,
we looked at saving content to a Uri
obtained by insert()
into the MediaStore
.
If we put data into the MediaStore
, it may also prove useful to get data
back out of it.
The basic recipe for this has not changed from past Android versions. You can use a ContentResolver
to query()
a MediaStore
collection of relevance.
For example, this sample project
has its own edition of VideoRepository
that
will query()
the MediaStore
for all videos on external storage:
suspend fun listTitles(): List<String>? =
withContext(Dispatchers.IO) {
val resolver = context.contentResolver
resolver.query(collection, PROJECTION, null, null, SORT_ORDER)
?.use { cursor ->
cursor.mapToList { it.getString(0) }
}
}
UPDATE 2020-03-03: This sample has since been modified in support of
demonstrating RecoverableSecurityException
.
So, while this source works, it no longer matches the repository.
In this function, inside of a coroutine, we:
-
Obtain a
ContentResolver
from a suitableContext
-
Call
query()
on thatContentResolver
to obtain aCursor
with interesting stuff -
Get the 0th column out of each
Cursor
row and return that in aList
PROJECTION
and SORT_ORDER
are just for the title:
private val PROJECTION = arrayOf(MediaStore.Video.Media.TITLE)
private const val SORT_ORDER = MediaStore.Video.Media.TITLE
…and mapToList()
just iterates over the Cursor
rows and applies the lambda
expression to each row:
private fun <T : Any> Cursor.mapToList(predicate: (Cursor) -> T): List<T> =
generateSequence { if (moveToNext()) predicate(this) else null }
.toList()
collection
, though, varies a bit by OS version, as on Android 10+ we can
get dedicated Uri
values for media by storage volume. Here, we get the value
for external storage:
private val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL
)
} else {
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
Where things get even more interesting is how this bit of code relates to permissions.
On Android 9 and older devices (back to Android 4.4 IIRC), if you run that code
without READ_EXTERNAL_STORAGE
(or perhaps WRITE_EXTERNAL_STORAGE
), you crash
with a SecurityException
. However, once you have that permission, your query will
return a list of the titles of all videos available in that collection.
On Android 10, you can run that code with no permissions. However, you will
only get back those pieces of content that your app inserted into the MediaStore
collection. Only if you hold READ_EXTERNAL_STORAGE
will you be able to get the list
of all titles in that collection, including those placed there by other apps or by the
user (e.g., via USB cable).
You can see this in action if you run that sample app on an Android 10 device. The
UI is mostly a RecyclerView
showing the list of titles, and that will be empty
the first time you run the app. If you click the “add to library” toolbar button,
the app will copy a small MP4 file from assets/
into the MediaStore
, using the
techniques from the previous blog post.
You will then see “test” show up in the RecyclerView
, as the media’s filename
is test.mp4
. If you click the “all inclusive” action bar item, you will be
prompted to grant read access to external storage. After that, the list will show
all videos in external storage, because now the app has READ_EXTERNAL_STORAGE
rights and can access all the content.
In contrast, if you run that app on Android 9 or older devices, you will be prompted
to grant READ_EXTERNAL_STORAGE
right away, as otherwise we cannot query()
the MediaStore
This sample app just needs the titles. If you wanted to actually do something
with the content, such as play it back using ExoPlayer, you can get a Uri
on the content by:
-
Including
MediaStore.Video.Media._ID
in your projection -
Obtaining the
_ID
column value for the piece of content of interest -
Pass that plus the collection
Uri
that you queried toContentUris.withAppendedId()
to get theUri
for that particular piece of content
If you were able to conduct the query, you will be able to read in the content.
If you want to use a third-party app to do something with the content – such as
passing the Uri
to a video player via ACTION_VIEW
– be sure to add
the Intent.FLAG_GRANT_READ_URI_PERMISSION
flag to the Intent
, to pass along
read access rights. Otherwise, the video player app may not have permission to work
with that Uri
.
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