Android Native - Query Audio Files from MediaStore

dimitrilc 1 Tallied Votes 26 Views Share
Introduction

In Android development, the MediaStore API is a great API to use if you are building a music player. In this tutorial, we will learn how to query for audio files in the MediaStore database.

Goals

At the end of the tutorial, you would have learned:

  1. How to query for Audio files from the MediaStore.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Intermediate Android
  2. Android permissions.
  3. ActivityResult API.
  4. Basic SQL.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Remove default “Hello World!” TextView.

  3. Add a new Button under ConstraintLayout.

  4. Constraint the Button to the center of the screen.

  5. Extract the Button android:text value to strings.xml. Set the Resource Name to button_startQuery and Resource Value to Start Query.

  6. Your activity_main.xml content should be similar to the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button_startQuery"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  7. Download the .mp3 file called Unexpected Gifts of Spring by Alextree from the FreeMusicArchive. This song is licensed under CC BY 4.0.

Project Overview

The frontend of our project is quite simple. It only has a single button. When the project is completed, our App should be able to perform the workflow below:

  1. User taps the Starts Query button.
  2. If the App has not been granted the runtime(dangerous) permission of android.permission.READ_EXTERNAL_STORAGE, the App will request the user to grant this permission.
  3. If the App has received the permission, it will then query the MediaStore database for entries matching the query parameters.
  4. The App will log the query results via the debugging channel.
READ_EXTERNAL_STORAGE

Because our App workflow requires the READ_EXTERNAL_STORAGE permission, so we will take care of that first. Based on the documentation, this permission is a runtime permission, so we must manually request it ourselves (API 23 or higher).

To add the permission request into our workflow, performs the steps below:

  1. In AndroidManifest.xml, add the <uses-permission> element inside of <manifest>, but outside of <application>.

     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  2. Open up the file MainActivity.kt. Inside of onCreate(), add a reference to the Button object.

     val button = findViewById<Button>(R.id.button)
  3. Below the button reference, add the code below to define an ActivityResultLauncher with an empty callback.

     val permissionResultLauncher = registerForActivityResult(RequestPermission()){ isGranted ->
    
     }
  4. Add the code below to bind the Button onClickListener to the permissionResultLauncher#launch() action.

     button.setOnClickListener {
        permissionResultLauncher.launch(READ_EXTERNAL_STORAGE)
     }
  5. You can run the app now if you wish just to see how the permission flow looks like. To clear the granted permission, either reinstall the app or use the adb revoke command below:

     adb shell pm revoke com.example.daniwebandroidqueryaudiomediastore android.permission.READ_EXTERNAL_STORAGE
MediaStore

The Android system has common public locations for Apps to store media files, such as Audio, Video, or Image. For this tutorial, we are only concerned about audio files; Android will automatically scan for audio files in the locations below.

  • Alarms/
  • Audiobooks/
  • Music/
  • Notifications/
  • Podcasts/
  • Ringtones/
  • Recordings/

After scanning, it stores information about these files inside an internal database called MediaStore. To interact with this database, we can use the MediaStore API. When interacting with the MediaStore database, there are a couple of important things to keep in mind:

  1. MediaStore only automatically refreshes its data at certain times, like a cold boot.
  2. Apps can write entries to the MediaStore even if the actual files do not exist. A file can be deleted in a way that does not trigger MediaStore to update itself. Proper checking/error handling is recommended when attempting to play a media file.
MediaStore Query

Even though we are interacting with the MediaStore database via the MediaStore API, just like any SQL query, we will need to create our query. To do this, follow the steps below:

  1. Inside the MainActivity class, create a new queryAudio() function.

     private fun queryAudio(){
     }
  2. To send a SQL query to the MediaStore database, we would use the query() function from the ContentResolver object. We can retrieve the ContentResolver from the App Context. Add the code below into queryAudio().

     applicationContext.contentResolver.query(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        projection,
        selection,
        selectionArgs,
        sortOrder
     )
  3. The query() function used here takes 5 arguments. Refer to the list below for a simplified explanation for them.
    a. uri: the location to query.
    b. projection: the columns that you want to be included.
    c. selection: this is where you put the SQL WHERE clause.
    d. selectionArgs: if you have any parameter used in the selection query (the previous function argument), you can provide values for them here.
    e. sortOrder: how you would like the result to be sorted.

  4. Now let us define the arguments before the query() call.

     val projection = arrayOf(
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.ALBUM
     )
    
     val selection = null //not filtering out any row.
     val selectionArgs = null //this can be null because selection is also null
     val sortOrder = null //sorting order is not needed
Use the Cursor to navigate

The query() call returns a Cursor object, so we will need to use it to get the data.

  1. Link the result of the query() call to a use clause (similar to Java try-with-resource) using the code below.

     applicationContext.contentResolver.query(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        projection,
        selection,
        selectionArgs,
        sortOrder
     )?.use { cursor ->
    
        val titleColIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
        val albumColIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)
    
        Log.d(TAG, "Query found ${cursor.count} rows")
    
        while (cursor.moveToNext()) {
            val title = cursor.getString(titleColIndex)
            val album = cursor.getString(albumColIndex)
    
            Log.d(TAG, "$title - $album")
        }
     }
  2. The code snippet above will log the title and the album values to a logging channel.

  3. Add the top-level TAG in MainActivity.kt.

     private const val TAG = "AUDIO_QUERY"
  4. Call the query() function in your ActivityResultLauncher callback.

     val permissionResultLauncher = registerForActivityResult(RequestPermission()){ isGranted ->
        if (isGranted) queryAudio()
     }
Upload the audio file

Remember the audio file that we had to download earlier? You can find its metadata in the screenshot below. We expect that our App will output the correct value for Title and Album.

1.png

For MediaStore to generate an entry, we will upload the mp3 file directly to the file system and then reboot the device so MediaStore will refresh.

  1. Boot your emulator now.
  2. In Android Studio, open the Device File Explorer from View > Tool Windows > Device File Explorer.
  3. Navigate to /storage/self/primary/Music.
  4. Right-click on Music > Upload > select the mp3 file.

2.png

  1. Cold reboot your emulator.
Run the App

Now run the app while filtering Logcat for AUDIO_QUERY, and you will see that the App correctly logs the output.

2022-01-12 16:58:58.571 8415-8415/com.example.daniwebandroidqueryaudiomediastore D/AUDIO_QUERY: Query found 1 rows
2022-01-12 16:58:58.571 8415-8415/com.example.daniwebandroidqueryaudiomediastore D/AUDIO_QUERY: Unexpected Gifts of Spring - Glint EP

MediaStore_Audio.gif

Solution Code

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.example.daniwebandroidqueryaudiomediastore">

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

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/Theme.DaniwebAndroidQueryAudioMediaStore">
       <activity
           android:name=".MainActivity"
           android:exported="true">
           <intent-filter>
               <action android:name="android.intent.action.MAIN" />

               <category android:name="android.intent.category.LAUNCHER" />
           </intent-filter>
       </activity>
   </application>

</manifest>

MainActivity.kt

package com.example.daniwebandroidqueryaudiomediastore

import android.Manifest.permission.READ_EXTERNAL_STORAGE
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission

private const val TAG = "AUDIO_QUERY"

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val button = findViewById<Button>(R.id.button)

       val permissionResultLauncher = registerForActivityResult(RequestPermission()){ isGranted ->
           if (isGranted) queryAudio()
       }

       button.setOnClickListener {
           permissionResultLauncher.launch(READ_EXTERNAL_STORAGE)
       }

   }

   private fun queryAudio(){
       val projection = arrayOf(
           MediaStore.Audio.Media.TITLE,
           MediaStore.Audio.Media.ALBUM
       )

       val selection = null //not filtering out any row.
       val selectionArgs = null //this can be null because selection is also null
       val sortOrder = null //sorting order is not needed

       applicationContext.contentResolver.query(
           MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
           projection,
           selection,
           selectionArgs,
           sortOrder
       )?.use { cursor ->

           val titleColIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
           val albumColIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)

           Log.d(TAG, "Query found ${cursor.count} rows")

           while (cursor.moveToNext()) {
               val title = cursor.getString(titleColIndex)
               val album = cursor.getString(albumColIndex)

               Log.d(TAG, "$title - $album")
           }
       }
   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button_startQuery"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Query Audio MediaStore</string>
   <string name="button_startQuery">Start Query</string>
</resources>
Summary

We have learned how to use the MediaStore API to query the MediaStore database. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidQueryAudioMediaStore

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, learning, and sharing knowledge.