Create a Music Player on Android: Project Setup

Android smartphones usually come with their own built-in apps to listen to music. However, you might not like all of their features. What if instead of downloading a third-party app, you could create your own music player in Android?

In this tutorial series, we will create a basic music player application for Android. The app will present a list of songs on the user device, so that the user can select a song to play. The app will also present controls for interacting with the playback and will continue playing when the user moves away from the app, with a notification displayed during playback.

Here is the final result that you will have after completing this series:

Android Music Player Final Result PreviewAndroid Music Player Final Result PreviewAndroid Music Player Final Result Preview

App Overview

Creating a music player is more complicated than creating something like a calculator. This is primarily because you can code or create everything that you will need in a calculator by yourself. However, in a music player, you will have to rely on the system to provide songs and play the music. Therefore, it is important to know how we can use different classes and services in Android to create a music player. There are also other differences, such as keeping the music going even if the user has navigated away from the app.

There are three important tasks that the app will have to do: play songs, control the song playback, and keep playing songs in the background. We will use the MediaPlayer class to play the songs and the MediaController class to control the playback of the songs. The Service class is also going to be an important part of our app as it will allow us to play songs in the background.

We will also need to get a list of songs from the user’s device. The ContentResolver class is going to be a great help here as it will allow us to fetch or modify content that comes from installed apps or the Android file system.

Project Setup

Begin by opening Android Studio and clicking on New Project. This will open a new window where you can select the type of activity. If you are using Android Studio Flamingo release, select Empty Views Activity as shown below. This allows us to create an empty activity that relies on XML files for the layout.

Android Studio Empty Views ActivityAndroid Studio Empty Views ActivityAndroid Studio Empty Views Activity

Click on Next, and then set the name of the app as Music Player. Select Kotlin as your Language and set the Minimum SDK to API 23. Finally, click the Finish button and wait for Android Studio to set up the project.

Music Player New Project ConfigurationMusic Player New Project ConfigurationMusic Player New Project Configuration

Requesting Permissions

Android has evolved over time to give users more control over the services that different applications can access. This means that you will need to ask for permission to do things like accessing the external storage or showing notifications to users.

Open the AndroidManifest.xml file of your project and add the following permissions just below the application tag.

1
<uses-permission android:name="android.permission.WAKE_LOCK" />
2
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
3
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
4
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
5
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

The WAKE_LOCK permission allows our app to keep the device active.

The READ_EXTERNAL_STORAGE permission allows the app to read data from the device’s external storage. This permission no longer works on newer Android versions.

The READ_MEDIA_AUDIO permission is required on newer Android versions (API level 33) to access audio files.

The POST_NOTIFICATIONS permission is required to allow an app to post notifications on the device.

Other Changes

You should also update the activity element of your AndroidManifest.xml file to have the following code:

1
<activity
2
    android:name=".MainActivity"
3
    android:exported="true"
4
    android:launchMode="singleTop"
5
    android:screenOrientation="portrait">
6
    <intent-filter>
7
        <action android:name="android.intent.action.MAIN" />
8
        <category android:name="android.intent.category.LAUNCHER" />
9
    </intent-filter>
10
</activity>
11

12
<service android:name="com.tutsplus.musicplayer.MusicService" />

Here, we are saying that the app has to run in portrait mode. We are also specifying that the app will have a service called MusicService. Make sure that the name of the service is in accordance with your package name.

Creating the App Layout

Our app will display a list of songs and have two buttons at the top to shuffle and stop the music. We will use a RecyclerView widget to display the song list. We already have a tutorial that covers the basics of RecyclerView if you haven’t used it before.

Here is the XML code that goes inside the activity_main.xml file:

1
<?xml version="1.0" encoding="utf-8"?>
2
<androidx.constraintlayout.widget.ConstraintLayout 
3
    xmlns:android="https://schemas.android.com/apk/res/android"
4
    xmlns:app="http://schemas.android.com/apk/res-auto"
5
    xmlns:tools="http://schemas.android.com/tools"
6
    android:layout_width="match_parent"
7
    android:layout_height="match_parent"
8
    tools:context=".MainActivity">
9

10

11
    <Button
12
        android:id="@+id/action_shuffle"
13
        android:layout_width="wrap_content"
14
        android:layout_height="wrap_content"
15
        android:layout_marginTop="10dp"
16
        android:layout_marginStart="20dp"
17
        android:text="Shuffle"
18
        android:onClick="shuffleSongs"
19
        app:layout_constraintStart_toStartOf="parent"
20
        app:layout_constraintTop_toTopOf="parent"/>
21

22
    <Button
23
        android:id="@+id/action_end"
24
        android:layout_width="88dp"
25
        android:layout_height="wrap_content"
26
        android:layout_marginTop="12dp"
27
        android:layout_marginEnd="20dp"
28
        android:text="Stop"
29
        android:onClick="stopSong"
30
        app:layout_constraintEnd_toEndOf="parent"
31
        app:layout_constraintTop_toTopOf="parent" />
32

33
    <androidx.recyclerview.widget.RecyclerView
34
        android:id="@+id/song_list"
35
        android:layout_width="match_parent"
36
        android:layout_height="match_parent"
37
        app:layout_constraintTop_toBottomOf="@id/action_shuffle"
38
        android:layout_marginTop="60dp"/>
39

40
</androidx.constraintlayout.widget.ConstraintLayout>

We are using a ConstraintLayout widget for the UI of the whole activity and a RecyclerView widget for the list of songs.

Create another file called song.xml inside the layout directory.

Song Item LayoutSong Item LayoutSong Item Layout

Place the following XML in it:

1
<?xml version="1.0" encoding="utf-8"?>
2
<RelativeLayout
3
    xmlns:android="http://schemas.android.com/apk/res/android"
4
    xmlns:tools="http://schemas.android.com/tools"
5
    android:layout_width="match_parent"
6
    android:layout_height="100dp"
7
    android:layout_margin="10dp"
8
    android:onClick="songPicked">
9

10
    <ImageView
11
        android:id="@+id/song_art"
12
        android:layout_width="80dp"
13
        android:layout_height="80dp"
14
        tools:srcCompat="@android:drawable/picture_frame" />
15
    <LinearLayout
16
        android:layout_width="match_parent"
17
        android:layout_height="wrap_content"
18
        android:layout_marginStart="20dp"
19
        android:layout_toEndOf="@+id/song_art"
20
        android:orientation="vertical">
21

22
        <TextView
23
            android:id="@+id/song_name"
24
            android:layout_width="wrap_content"
25
            android:layout_height="wrap_content"
26
            android:fontFamily="sans-serif-condensed-medium"
27
            android:textColor="@color/brown"
28
            android:textSize="20sp"
29
            tools:text="Song Name" />
30
        <TextView
31
            android:id="@+id/song_artist"
32
            android:layout_width="wrap_content"
33
            android:layout_height="wrap_content"
34
            android:textColor="@color/teal"
35
            android:textSize="16sp"
36
            tools:text="Artist" />
37
        <TextView
38
            android:id="@+id/song_length"
39
            android:layout_width="wrap_content"
40
            android:layout_height="wrap_content"
41
            android:textColor="@color/grey"
42
            android:textSize="14sp"
43
            android:fontFamily="monospace"
44
            tools:text="03:30" />
45
    </LinearLayout>
46

47
</RelativeLayout>

This XML determines the layout for individual songs in our app. We display their album art on the left side and information about the song on the right.

Get a List of Songs

Let’s create a data class called Songs first. The instances of this class will help us store the information about different songs. We will also create an array to store all our songs. Add this line below the MainActivity class:

1
data class Song(val id: Long, val name: String, val duration: String, val artist: String, val cover: Uri)

Inside the MainActivity class, add the following line at the top:

1
private val songs: ArrayList<Song> = arrayListOf()
2
private val MY_PERMISSIONS_REQUEST_READ_MEDIA_AUDIO = 1
3
private lateinit var recyclerView: RecyclerView

We will now define a function that can get us a list of songs from the device. This function will get audio files from a specific folder in the external storage. The audio files need to be at least 15 seconds in length to be included in the list of songs.

Add this code inside your MainActivity class:

1
private fun getSongList() {
2
    val musicResolver = contentResolver
3
    val musicUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
4

5
    val selection = "${MediaStore.Audio.Media.DATA} LIKE ? AND ${MediaStore.Audio.Media.DURATION} >= ?"
6
    val selectionArgs = arrayOf("%/Music/%", "15000")
7
    val musicCursor = musicResolver.query(musicUri, null, selection, selectionArgs, null)
8

9
    if ((musicCursor != null) && musicCursor.moveToFirst()) {
10
        val titleColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
11
        val idColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media._ID)
12
        val durationColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.DURATION)
13
        val artistColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)
14
        val albumIdColumn = musicCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID)
15

16
        do {
17
            val thisId = musicCursor.getLong(idColumn)
18
            val thisTitle = musicCursor.getString(titleColumn)
19
            val thisDuration = musicCursor.getString(durationColumn)
20
            val thisArtist = musicCursor.getString(artistColumn)
21
            val thisAlbumId = musicCursor.getString(albumIdColumn)
22

23
            val albumArtUri = Uri.parse("content://media/external/audio/albumart")
24
            val albumArtContentUri = ContentUris.withAppendedId(albumArtUri, thisAlbumId.toLong())
25
            songs.add(Song(thisId, thisTitle, thisDuration, thisArtist, albumArtContentUri))
26
            
27
        } while (musicCursor.moveToNext())
28

29
        musicCursor.close()
30
    } else {
31
        Log.d("MyTag", "The song list is empty")
32
    }
33
}

We begin by creating an instance of the contentResolver class. As I mentioned earlier, this class allows us to work with content from a variety of sources such as the Android file system, installed apps, and other supported APIs.

After that, we execute a query to only select files from a specific directory with a minimum length of 15 seconds. The Cursor object that we get back contains the results of the query. We check if the results aren’t empty and then proceed to get information about individual songs.

All this information is stored in an instance of the Songs class which is then added to the songs array we defined earlier.

Displaying the List of Songs

We will now implement our RecyclerView adapter to populate our widget with a list of songs. Add the following code below the Song data class:

1
class RVAdapter(private val songs: List<Song>) :
2
    RecyclerView.Adapter<RVAdapter.SongViewHolder>() {
3
    class SongViewHolder(itemView: View) :
4
        RecyclerView.ViewHolder(itemView) {
5
        var songName: TextView = itemView.findViewById(R.id.song_name)
6
        var songLength: TextView = itemView.findViewById(R.id.song_length)
7
        var songArtist: TextView = itemView.findViewById(R.id.song_artist)
8
        var songCover: ImageView = itemView.findViewById(R.id.song_art)
9
    }
10
    override fun getItemCount(): Int {
11
        return songs.size
12
    }
13
    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): SongViewHolder {
14
        val v: View =
15
            LayoutInflater.from(viewGroup.context).inflate(R.layout.song, viewGroup, false)
16
        return SongViewHolder(v)
17
    }
18
    override fun onBindViewHolder(songViewHolder: SongViewHolder, idx: Int) {
19
        val duration_minutes_seconds = "${(songs[idx].duration.toInt()/(60*1000)).toString().padStart(2, '0')}:${(songs[idx].duration.toInt()%60).toString().padStart(2, '0')}";
20
        songViewHolder.songName.text = songs[idx].name
21
        songViewHolder.songLength.text = duration_minutes_seconds
22
        songViewHolder.songArtist.text = songs[idx].artist
23
        songViewHolder.songCover.setImageURI(songs[idx].cover)
24
        songViewHolder.itemView.tag = idx
25
    }
26
}

This class is similar in implementation to our class from the RecyclerView tutorial. However, we have modified it a bit to display the data for our songs instead of people.

The SongViewHolder class holds references to the different views that make up the layout of each of our song items. The onBindViewHolder() method binds the data stored in each of our Song objects to different view holders.

Newer versions of Android require us to ask for some permissions at runtime. We will therefore add the code to ask for permission inside the onCreate() method of MainActivity.

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

5
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_AUDIO)
6
        != PackageManager.PERMISSION_GRANTED) {
7

8
        if (ActivityCompat.shouldShowRequestPermissionRationale(this,
9
                Manifest.permission.READ_MEDIA_AUDIO)) {
10
            // Explain to Users Why You Need Permissions
11
        } else {
12
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
13
                ActivityCompat.requestPermissions(this,
14
                    arrayOf(Manifest.permission.READ_MEDIA_AUDIO),
15
                    MY_PERMISSIONS_REQUEST_READ_MEDIA_AUDIO)
16
            }
17
        }
18
    } else {
19
        displaySongs()
20
    }
21
}

We begin by checking if the permission has been granted. If the permission hasn’t been granted, we can either give users an explanation or we can directly ask for the permissions. In this tutorial, we are directly asking for the permissions for brevity.

If the permission has been granted already, we simply call the displaySongs() method to display the songs.

A user will either grant or reject the permission request. In either case, you need to respond with a suitable action plan. The onRequestPermissionsResult() method helps us tackle both these scenarios.

1
override fun onRequestPermissionsResult(requestCode: Int,
2
                                        permissions: Array<String>, grantResults: IntArray) {
3
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
4
    when (requestCode) {
5
        MY_PERMISSIONS_REQUEST_READ_MEDIA_AUDIO -> {
6
            if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
7
                displaySongs()
8
            } else {
9
                // Handle Denial of Permission
10
            }
11
            return
12
        }
13
    }
14
}

For the sake of simplicity, I am just handling the case where users grant the permissions. In this case, we simply call the displaySongs() method to display the songs.

Here is the code for the displaySongs() method:

1
private fun displaySongs() {
2
    recyclerView = findViewById(R.id.song_list)
3
    recyclerView.setHasFixedSize(true)
4

5
    getSongList()
6

7
    songs.sortWith { a, b -> a.name.compareTo(b.name) }
8

9
    val linearLayoutManager = LinearLayoutManager(this)
10
    recyclerView.layoutManager = linearLayoutManager
11

12
    val adapter = RVAdapter(songs)
13
    recyclerView.adapter = adapter
14
}

Inside this method, we store a reference to our RecyclerView widget in the recyclerView variable. After that, we call the getSongList() method to get a list of songs. We sort the songs alphabetically and then lay them out inside the app using our adapter.

Final Thoughts

We’ve now set the app up to read songs from the user device. In the next part, we will begin playback when the user selects a song using the MediaPlayer class. We will implement playback using a Service class so that it will continue as the user interacts with other apps. Finally, we will use a MediaController class to give the user control over playback.