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 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 }

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 =
  } else {

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.

The entire series of “Scoped Storage Stories” posts includes posts on:

Stuck on an Android problem? Subscribers have access to live office hours chats with Mark Murphy, to help you work through your challenges!