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) }
}
}
In this function, inside of a coroutine, we:
Obtain a ContentResolver from a suitable Context
Call query() on that ContentResolver to obtain a Cursor with interesting stuff
Get the 0th column out of each Cursor row and return that in a List
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 to ContentUris.withAppendedId()
to get the Uri 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.
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
In the next MediaStore post, I will look at modifying content placed on the
device by the user or by other apps, as this is a bit different than storing
your own content.


