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:
- How to query for Audio files from the MediaStore.
Tools Required
- Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
- Intermediate Android
- Android permissions.
- ActivityResult API.
- Basic SQL.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Remove default “Hello World!” TextView.
-
Add a new Button under ConstraintLayout.
-
Constraint the Button to the center of the screen.
-
Extract the Button
android:text
value to strings.xml. Set the Resource Name tobutton_startQuery
and Resource Value toStart Query
. -
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>
-
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:
- User taps the Starts Query button.
- 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. - If the App has received the permission, it will then query the MediaStore database for entries matching the query parameters.
- 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:
-
In AndroidManifest.xml, add the
<uses-permission>
element inside of<manifest>
, but outside of<application>
.<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
-
Open up the file MainActivity.kt. Inside of onCreate(), add a reference to the Button object.
val button = findViewById<Button>(R.id.button)
-
Below the
button
reference, add the code below to define an ActivityResultLauncher with an empty callback.val permissionResultLauncher = registerForActivityResult(RequestPermission()){ isGranted -> }
-
Add the code below to bind the Button
onClickListener
to thepermissionResultLauncher#launch()
action.button.setOnClickListener { permissionResultLauncher.launch(READ_EXTERNAL_STORAGE) }
-
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:
- MediaStore only automatically refreshes its data at certain times, like a cold boot.
- 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:
-
Inside the MainActivity class, create a new
queryAudio()
function.private fun queryAudio(){ }
-
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 intoqueryAudio()
.applicationContext.contentResolver.query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, sortOrder )
-
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 SQLWHERE
clause.
d. selectionArgs: if you have any parameter used in theselection
query (the previous function argument), you can provide values for them here.
e. sortOrder: how you would like the result to be sorted. -
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.
-
Link the result of the
query()
call to ause
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") } }
-
The code snippet above will log the title and the album values to a logging channel.
-
Add the top-level
TAG
in MainActivity.kt.private const val TAG = "AUDIO_QUERY"
-
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.
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.
- Boot your emulator now.
- In Android Studio, open the Device File Explorer from View > Tool Windows > Device File Explorer.
- Navigate to
/storage/self/primary/Music
. - Right-click on Music > Upload > select the mp3 file.
- 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
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