Scoped Storage Stories: Storing 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 eighth post in a series where we will explore how to work with those alternatives, now looking at MediaStore options.

The Storage Access Framework offers the user the most flexibility of where your app’s content should be placed. One key downside is that it requires the user to interact with some system UI to choose where to place the content.

getExternalFilesDir() and related Context methods avoid that UI. However, the files that you create in these locations get removed when the app is uninstalled. That may not be appropriate for all types of content, as the user may get irritated if uninstalling the app deletes “their” files.

The third option is MediaStore . As with the Storage Access Framework, content you store via MediaStore will remain after the app is uninstalled. And, like getExternalFilesDir() and kin, you are not forced to display some system UI. However, MediaStore is restricted (mostly) to just media: images, audio files, and video files.

The basic recipe for storing content using MediaStore is:

Create a ContentValues describing the content

insert() that into a MediaStore collection using a ContentResolver

Use the Uri that insert() returns to open an OutputStream (again using ContentResolver )

Write your content to that OutputStream

This is definitely more complicated than simply using a File , but it’s not that bad.

This sample app (from Elements of Android Q) lets you download archived conference videos from my Web server. The app is set up to use Environment.getExternalStoragePublicDirectory() on older devices but use MediaStore on Android 10 and above.

First, you need a collection Uri from the MediaStore , representing where you want to store the content. Conference videos are videos (surprise!), so the VideoRepository uses MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) to get a Uri representing the storage location for video content on external storage. getContentUri() is a new method in Android 10, and you can use it to work both with external and removable storage.

The VideoRepository has a downloadQ() function that derives a URL of a video from a filename and downloads the video into that collection:

private suspend fun downloadQ ( filename : String ): Uri = withContext ( Dispatchers . IO ) { val url = URL_BASE + filename val response = ok . newCall ( Request . Builder (). url ( url ). build ()). execute () if ( response . isSuccessful ) { val values = ContentValues (). apply { put ( MediaStore . Video . Media . DISPLAY_NAME , filename ) put ( MediaStore . Video . Media . RELATIVE_PATH , "Movies/ConferenceVideos" ) put ( MediaStore . Video . Media . MIME_TYPE , "video/mp4" ) put ( MediaStore . Video . Media . IS_PENDING , 1 ) } val resolver = context . contentResolver val uri = resolver . insert ( collection , values ) uri ?. let { resolver . openOutputStream ( uri ) ?. use { outputStream -> val sink = Okio . buffer ( Okio . sink ( outputStream )) response . body () ?. source () ?. let { sink . writeAll ( it ) } sink . close () } values . clear () values . put ( MediaStore . Video . Media . IS_PENDING , 0 ) resolver . update ( uri , values , null , null ) } ?: throw RuntimeException ( "MediaStore failed for some reason" ) uri } else { throw RuntimeException ( "OkHttp failed for some reason" ) } }

This sample uses coroutines, so all the work is wrapped in a Dispatchers.IO coroutine context, to arrange for it to be performed on a backgorund thread.

downloadQ() gets the URL based on the filename, then uses OkHttp to make the HTTP GET request to download it.

If we get a 200 OK response, we then set up a ContentValues representing this new bit of media. The DISPLAY_NAME is just our filename, and the MIME_TYPE is tied to the type of media (in this case, an MP4 video). RELATIVE_PATH is new to Android 10 and allows you to specify a sub-folder within the collection where this content should be placed – in this case, we are asking for a ConferenceVideos folder within Movies . IS_PENDING is also new to Android 10, where a value of 1 means that the content is not yet ready for use, as we are still downloading it.

After we insert() that ContentValues using a ContentResolver , we have a Uri representing where the MediaStore wants us to place the actual content. We can use the ContentResolver and openOutputStream() to get an OutputStream on that location. From there, we use Okio to slurp down the video content and stream it out to the, um, stream. In a streaming fashion. Streamily.

Finally, we replace the ContentValues content with IS_PENDING set to 0 and use the ContentResolver to update() the values associated with our Uri . This flip IS_PENDING to false, meaning that the content is ready for use.

This function can then be called from a viewmodel or other place that has a suitable coroutine scope (e.g., viewModelScope ).

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

Nervous about how the newest version of Android affects your app? Consider subscribing, then asking questions in the office hours chats!

— Dec 21, 2019