How to Create Media
It used to be that creating media was a matter of writing the media to some file on external storage, then getting the MediaStore
to index it (e.g., via MediaScannerConnection
). However, now that writing to external storage is less of an option, we need to switch techniques. The technique that the Android 10 documentation cites is to use insert()
on ContentResolver
… though that approach only works on Android 10, as you will see.
Getting the Root Uri
Once again, you will need a Uri
identifying the collection (audio, image, video) of the content that you want to create, and possibly the storage volume on which to create it. This is the same as for querying, where you can use getContentUri()
or try one of the legacy constants (e.g., MediaStore.Video.Media.EXTERNAL_CONTENT_URI
).
The ConferenceVideos
sample uses the same collection
value that we used for querying:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL
) else MediaStore.Video.Media.EXTERNAL_CONTENT_URI
Crafting the Metadata
insert()
takes a ContentValues
that describes the content that you wish to add to the media collection identified by the Uri
that you supply. At minimum, your ContentValues
needs to provide DISPLAY_NAME
(for a human-readable identifier for this content) and MIME_TYPE
(to indicate what sort of content this is).
Android 10 adds a few additional options that you can consider.
IS_PENDING
One is IS_PENDING
. As the name suggests, this indicates whether or not the content is “pending”. “Pending” implies that the content is not yet ready for use — for example, the MediaStore
database entry is created, but we are still downloading the content. IS_PENDING
controls whether other apps, querying the MediaStore
, will see this entry:
-
1
means that the content is pending, and other apps will not see this content by default (unless they usesetIncludePending()
) -
0
means that the content is ready for use
Hence, the recipe is to set IS_PENDING
to 1
initially, then flip it to 0
via an update()
call on a ContentResolver
when the content is ready for consumption by other apps.
Directory Hints
Android 10 also offers RELATIVE_PATH
. This addresses a key problem with MediaStore
: controlling where the media goes on the device.
Suppose you are downloading MP3 files representing songs. Typically, those would go in Music/[artist]/[album]/
as a directory, substituting in suitable values for <artist>
and <album>
. However, if the content itself lacks the metadata (e.g., MP3 tags), MediaStore
will not realize that the MP3 should go in this location.
RELATIVE_PATH
allows your code to assemble that relative path and suggest it to MediaStore
. It should be a relative path from a storage root (e.g., the root of external storage) that identifies a directory which MediaStore
should create (if needed) and use for the content.
This is a hint, and there is no requirement for MediaStore
to honor the request. In particular, if the top-level path segment of the relative path makes no sense (e.g., Stuff/Goes/Here
), MediaStore
may elect to ignore the request.
Using the Media Content Uri
insert()
returns a Uri
that represents where you can write your content. You can use that with openOutputStream()
or openFileDescriptor()
to write your content to the designated location. Afterwards, if you set IS_PENDING
to 1
, you can use update()
and that Uri
to reset it to 0
and allow other apps to see your content.
The VideoRepository
has a downloadQ()
function that:
- Assembles a URL to the video
- Uses OkHttp to request that video
- Uses
insert()
to get theUri
to where the video should be saved, withRELATIVE_PATH
suggesting to put the video in aConferenceVideos/
directory off of the stockMovies/
directory - Writes the video content to that location
- Uses
update()
to setIS_PENDING
to0
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")
}
}
Backwards-Compatibility Woe
Unfortunately, this does not work well on prior versions of Android:
- While what you write to the
Uri
location is saved to disk, it is not obvious where that location actually is - The content metadata, such as the filename, does not seem to be saved in
MediaStore
So, in the sample, downloadQ()
is wrapped by a download()
function that only uses downloadQ()
on Android 10 devices:
suspend fun download(filename: String): Uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) downloadQ(filename)
else downloadLegacy(filename)
download()
delegates to downloadLegacy()
on older devices, using the classic approach of writing the content to external storage, then indexing the result:
private suspend fun downloadLegacy(filename: String): Uri =
withContext(Dispatchers.IO) {
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES),
filename
)
val url = URL_BASE + filename
val response = ok.newCall(Request.Builder().url(url).build()).execute()
if (response.isSuccessful) {
val sink = Okio.buffer(Okio.sink(file))
response.body()?.source()?.let { sink.writeAll(it) }
sink.close()
MediaScannerConnection.scanFile(
context,
arrayOf(file.absolutePath),
arrayOf("video/mp4"),
null
)
FileProvider.getUriForFile(context, AUTHORITY, file)
} else {
throw RuntimeException("OkHttp failed for some reason")
}
}
However, we no longer have a Uri
representing the video this way, and the indexing operation is asynchronous. So, we settle for FileProvider
to give us access in the short term. Future runs of the app should see the content in the MediaStore
and be able to use a MediaStore
-supplied Uri
to work with it.
Also note that we use MediaScannerConnection.scanFile()
in this scenario. That is not needed with downloadQ()
, as we are directly putting the image into the MediaStore
.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.