Computer scienceMobileAndroidPersistence

MediaStore

14 minutes read

It's evident when an app needs to access its own files. But what if an app needs to access a file created by other apps? This is a job for shared storage. Android categorizes two types of files that can be stored in shared storage: media files and non-media files (documents and other formats).

Media files in shared storage can be stored and accessed using the MediaStore API, while other files, like documents, can be stored and accessed using the Storage Access Framework (SAF). In this topic, you'll focus on how to work with MediaStore. Let's begin!

MediaStore collections

A variety of apps boost the user experience by allowing you to contribute and access media files. This can be achieved with the MediaStore: an abstraction provided by Android for interacting with media files stored on the device in a structured and optimized manner. Consider it as a database containing well-defined tables, known as collections, which streamline the retrieval and updating of media files.

The system automatically scans the external storage and categorizes files into one of these four collections:

  1. MediaStore.Images: this collection includes images, such as camera photos and screenshots. You can find these files in the DCIM/ and Pictures/ directories.

  2. MediaStore.Video: this collection includes videos. These files are housed the DCIM/, Movies/, and Pictures/ directories.

  3. MediaStore.Audio: this collection consists of:

    • Audio files, located in the Alarms/, Audiobooks/, Music/, Notifications/, Podcasts/, and Ringtones/ directories.

    • Audio playlists, located in the Music/ or Movies/ directories.

    • Voice recordings, located in the Recordings/ directory.

  4. MediaStore.Downloads: this is a new collection introduced in Android 10. It includes all downloaded files, which you can find in the Download/ directory. Unlike the files in previous collections, which you can access using the MediaStore API, the suggested method to access these files is by using the Storage Access Framework.

As you can see, all collections use a similar syntax: MediaStore.<media-type>.

  • Please note that the Recordings/ directory isn't available on Android 11 (API level 30) and earlier versions.

  • The MediaStore.Downloads table isn't available on Android 9 (API level 28) and earlier versions.

Next, let's explore how you can use these collections.

Necessary permissions

For an app to take advantage of the media store, it requires certain permissions to be included in the AndroidManifest.xml file:

  1. <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />: you only need this permission if your app needs to access images or photos created by other apps.

  2. <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />: you only need this permission if your app needs to access videos or photos created by other apps.

  3. <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />: You only need this permission if your app needs to access audios created by other apps.

In addition to one or more permissions listed above, every app must declare these two permissions:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
                 android:maxSdkVersion="32" />

This permission is managed in two ways:

  • If your app is running on Android 10 or higher and using scoped storage, this permission is required to allow your app to access files (whether they are media files or not) created by other apps.

  • If your app is running on Android 9 and earlier, this permission is required to allow your app to access files in all different locations (whether those files are created by the app itself, or by other apps)

Starting from Android 13 (API level 33), this permission is no longer required, so we add android:maxSdkVersion="32", which means it won't be used on devices running on Android 13 or later.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="28" />

This permission is required to allow your app to modify media files. It's only required when your app is running on Android 9 ( API level 28) or earlier. That's why we add android:maxSdkVersion="28".

In summary, here's an example of the complete permissions needed for an app that wants to interact only with images:

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
                 android:maxSdkVersion="32" />

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="28" />

Querying collections

The MediaStore API is a content provider requiring the ContentResolver class for communication. The ContentResolver offers methods for querying and modifying data in the media store.

Let's break down the process of querying the MediaStore.Images collection. First, we need to define five variables that will be passed as parameters into the query() method, which are:

  • uri. This is the URI of the table we are interested in querying.

Since we are interested the MediaStore.Images table, we write:

val uri =
    if (Build.VERSION.SDK_INT >= 29) {
        MediaStore.Images.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL
        )
    } else {
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }

As you can notice, the way of specifying the Uri has changed starting from Android 1010.

  • projection. This parameter specifies the columns in the MediaStore.Images table from which we want to extract data.

For instance, if we want to extract the name (DISPLAY_NAME) and size (SIZE) of each image file, we will write:

val projection = arrayOf(
    MediaStore.Images.Media._ID,
    MediaStore.Images.Media.DISPLAY_NAME,
    MediaStore.Images.Media.SIZE
)

The _ID field must be present because we will need it to construct a content Uri for each image that we query.

  • selection. This is similar to the WHERE clause used to return specific rows from an SQL database.

For instance, if we want to extract only images that are less than 1 MB in size, we'll write:

val selection = "${MediaStore.Images.Media.SIZE} <= ?"

We can append multiple conditions using the logical operators AND and OR. We can also negate a condition using NOT. Here is an example:

val selection = "NOT ${MediaStore.Audio.Media.SIZE} <= ? AND ${MediaStore.Audio.Media.DURATION} > ?"
  • selectionArgs. This parameter complements the previous one by providing values to replace the ? placeholder. If we have more than one placeholder, they will be replaced in the order they appeared.

Following the condition we specified in the selection parameter, we write:

val selectionArgs = arrayOf(
    (1024 * 1024).toString()
)
  • sortOrder. Here we specify the order in which we want to extract the images. This is also similar to the ORDER BY clause used in a SQL database.

For example, if we want to order our images alphabetically in ascending order, we'll write:

val sortOrder = "${MediaStore.Images.Media.DISPLAY_NAME} ASC"

You can choose any name for these five variables. We've chosen names here that match the signature of the query() method for clarity.

Now we can call the query() method from the contentResolver class, passing in all the variables declared so far as parameters:

val query = applicationContext.contentResolver.query(
    uri,
    projection,
    selection,
    selectionArgs,
    sortOrder
)

The query() method will return a Cursor?, which you are familiar with from the SQLite topic. Let's first create a class to hold the data returned by the cursor object when iterating over our collection of images:

class Image(
    val contentUri: Uri,
    val displayName: String,
    val size: Int
)

Notice that we have specified a Uri property to store the Uri of the returned images for further processing.

The recommended practice when dealing with closable objects like Cursor is to use the use() function, which automatically closes them.

Now you're ready to iterate over the collection and save the results to an images list:

val images = mutableListOf<Image>()

query?.use { cursor ->
    // First, we cache column indices.
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
    val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
    val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)

    while (cursor.moveToNext()) {
        // Then, we get the values of columns for a given image.
        val id = cursor.getLong(idColumn)
        val displayName = cursor.getString(displayNameColumn)
        val size = cursor.getInt(sizeColumn)

        val contentUri = ContentUris.withAppendedId(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id
        )
        
        // Finally, we store the result in our defined list.
        images.add(Image(contentUri, displayName, size))
    }
}

With that, you've tackled the most challenging part. Now let's proceed to other operations with the media store.

Adding to collections

You can use the MediaStore API to enable your app to store the media files it creates in a specific collection. Let's look at the scenario of storing an audio file in the MediaStore.Audio table:

First, remember that you always need an instance of the contentResolver class to interact with the MediaStore abstraction, so let's create one:

val resolver = applicationContext.contentResolver

Next, we obtain the Uri for the MediaStore.Audio table:

val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

Note that Build.VERSION_CODES.Q corresponds to API level 29 (Android 10). Here, we used VOLUME_EXTERNAL_PRIMARY, which allows us to read and modify its content, as opposed to VOLUME_EXTERNAL, which only allows read operations (refer to the previous section).

Now, we create a ContentValues object and set the DISPLAY_NAME field to "Hyper music.mp3":

val hyperMusic = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "Hyper music.mp3")
}

ContentValues is used to define data for each column in a row of a relational database. In this case, it defines the DISPLAY_NAME of the new audio file.

Finally, we insert our new audio file in the corresponding collection:

val hyperMusicUri = resolver
        .insert(audioCollection, hyperMusic)

Notice here that the insert() method returns the Uri for the new file, allowing us to use it for further processing.

In a nutshell, here are the necessary steps to add a new item:

  1. Obtain the URI for the targeted media collection.

  2. Prepare the data of the new item in the form of a ContentValues() object.

  3. insert the ContentValues() object in the targeted collection via its URI.

Updating a file in a collection

What if we put the hyperMusicUri from our previous example to good use? For example, let's use it to update the name of the file in the collection. To achieve this, we need to prepare the updated data in the form of ContentValues() object as you've previously seen:

val updatedHyperMusic = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "Updated hyper music.mp3")
}

Next, we call the update() method, passing the necessary parameters:

hyperMusicUri?.let { uri ->
    val numUpdatedRows = resolver.update(
        uri,
        updatedHyperMusic,
        null,
        null
    )
}

For clarity, the code above is similar to this:

// This code will cause a compilation error
// because the `Uri` parameter is not nullable
// while `hyperMusicUri` is nullable.
val numUpdatedRows = resolver.update(
    hyperMusicUri,
    updatedHyperMusic,
    null,
    null
)

update() will return an integer representing the number of rows that were successfully updated. This can be used to verify if the update was successful. If not, you can write code to handle that.

Also, we passed null to the where (same as selection) and selectionArgs parameters that you are familiar with from the previous sections, because we don't need them. However, let's assume that we have the ID of the item we want to update (which can be obtained using the query() method discussed earlier). In that case, we can specify where and selectionArgs as follows:

val mediaId: Long = // The id of the item we need to update

val where = "${MediaStore.Audio.Media._ID} = ?"

val selectionArgs = arrayOf(mediaId.toString())

hyperMusicUri?.let { uri ->
    val numUpdatedRows = resolver.update(
        uri,
        updatedHyperMusic,
        where,
        selectionArgs
    )
}

Deleting a file from a collection

Similarly to updating a file, we can also use hyperMusicUri to delete the audio file from audioCollection. Here's the code for that:

hyperMusicUri?.let { uri ->
    val numRemovedRows = resolver.delete(
        uri,
        null,
        null
    )
}

Here, numRemovedRows also returns the number of successful file deletions (which should be 1 in our case). We passed null for both the where and selectionArgs parameters. You've already seen how they can be defined.

Conclusion

In this topic, you have learned how to access and store media files in shared storage using the MediaStore API. You've also learned that Android automatically organizes files inside shared storage into four collections: Images, Audio, Video, and Downloads. You have discovered the necessary permissions to include in the AndroidManifest.xml file, and how to query, insert, update, and delete from a media collection. All that remains is a little practice. Let's get to it right away!

9 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo