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:
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.
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.
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-permissionandroid:name="android.permission.WAKE_LOCK"/> |
2 | <uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAGE"/> |
3 | <uses-permissionandroid:name="android.permission.READ_MEDIA_AUDIO"/> |
4 | <uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE"/> |
5 | <uses-permissionandroid: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 | <actionandroid:name="android.intent.action.MAIN"/> |
8 | <categoryandroid:name="android.intent.category.LAUNCHER"/> |
9 | </intent-filter> |
10 | </activity> |
11 | |
12 | <serviceandroid: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.
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 | dataclassSong(valid:Long,valname:String,valduration:String,valartist:String,valcover:Uri) |
Inside the MainActivity
class, add the following line at the top:
1 | privatevalsongs:ArrayList<Song>=arrayListOf() |
2 | privatevalMY_PERMISSIONS_REQUEST_READ_MEDIA_AUDIO=1 |
3 | privatelateinitvarrecyclerView: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 | privatefungetSongList(){ |
2 | valmusicResolver=contentResolver |
3 | valmusicUri=MediaStore.Audio.Media.EXTERNAL_CONTENT_URI |
4 | |
5 | valselection="${MediaStore.Audio.Media.DATA} LIKE ? AND ${MediaStore.Audio.Media.DURATION} >= ?" |
6 | valselectionArgs=arrayOf("%/Music/%","15000") |
7 | valmusicCursor=musicResolver.query(musicUri,null,selection,selectionArgs,null) |
8 | |
9 | if((musicCursor!=null)&&musicCursor.moveToFirst()){ |
10 | valtitleColumn=musicCursor.getColumnIndex(MediaStore.Audio.Media.TITLE) |
11 | validColumn=musicCursor.getColumnIndex(MediaStore.Audio.Media._ID) |
12 | valdurationColumn=musicCursor.getColumnIndex(MediaStore.Audio.Media.DURATION) |
13 | valartistColumn=musicCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST) |
14 | valalbumIdColumn=musicCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID) |
15 | |
16 | do{ |
17 | valthisId=musicCursor.getLong(idColumn) |
18 | valthisTitle=musicCursor.getString(titleColumn) |
19 | valthisDuration=musicCursor.getString(durationColumn) |
20 | valthisArtist=musicCursor.getString(artistColumn) |
21 | valthisAlbumId=musicCursor.getString(albumIdColumn) |
22 | |
23 | valalbumArtUri=Uri.parse("content://media/external/audio/albumart") |
24 | valalbumArtContentUri=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 | classRVAdapter(privatevalsongs:List<Song>): |
2 | RecyclerView.Adapter<RVAdapter.SongViewHolder>(){ |
3 | classSongViewHolder(itemView:View): |
4 | RecyclerView.ViewHolder(itemView){ |
5 | varsongName:TextView=itemView.findViewById(R.id.song_name) |
6 | varsongLength:TextView=itemView.findViewById(R.id.song_length) |
7 | varsongArtist:TextView=itemView.findViewById(R.id.song_artist) |
8 | varsongCover:ImageView=itemView.findViewById(R.id.song_art) |
9 | } |
10 | overridefungetItemCount():Int{ |
11 | returnsongs.size |
12 | } |
13 | overridefunonCreateViewHolder(viewGroup:ViewGroup,viewType:Int):SongViewHolder{ |
14 | valv:View= |
15 | LayoutInflater.from(viewGroup.context).inflate(R.layout.song,viewGroup,false) |
16 | returnSongViewHolder(v) |
17 | } |
18 | overridefunonBindViewHolder(songViewHolder:SongViewHolder,idx:Int){ |
19 | valduration_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 | overridefunonCreate(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 | overridefunonRequestPermissionsResult(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 | privatefundisplaySongs(){ |
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 | vallinearLayoutManager=LinearLayoutManager(this) |
10 | recyclerView.layoutManager=linearLayoutManager |
11 | |
12 | valadapter=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.