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 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
.
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()
onDocumentFile
- 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.
#Google #Android #Smartphones #OS #News @ndrdnws #ndrdnws #AndroidNews