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:

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:

  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:

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.