In this series, we are creating a music player on Android using the MediaPlayer and MediaController classes. In the previous tutorial of this series, we learned how to get a list of songs stored on the user's external storage and display it to them.
In this tutorial, we will implement the functionality to play a song selected by the user. However, we want the song to keep playing even if the user isn't directly interacting with the app. We will have to implement a Service class to do this, and that's what we will do here.
Here is the final result of this series:
Creating a Service
You might remember from the previous tutorial that we added a service
element to our AndroidManifest.xml file using the line below.
1 | <serviceandroid:name="com.tutsplus.musicplayer.MusicService"/> |
We will now implement that service.
The first step involves the creation of a new class called MusicService
in your application. This class will inherit from the base Service
class. There are some additional interfaces that we have to implement. So our class declaration line should look like this:
1 | classMusicService:Service(),MediaPlayer.OnPreparedListener,MediaPlayer.OnErrorListener,MediaPlayer.OnCompletionListener{ |
Android Studio will then give you an error. Hover over the error and then click on the Implement members button to implement any unimplemented methods. Add these four variables to the MusicService
class.
1 | privatelateinitvarmediaPlayer:MediaPlayer |
2 | privatelateinitvarsongs:ArrayList<Song> |
3 | privatevarsongPosition=0 |
4 | |
5 | privatevalmusicBinder:IBinder=MusicBinder(this) |
We are using the lateinit
keyword to indicate to Kotlin that the variable might not be initialized now but we guarantee that we will initialize it later. This helps us avoid unnecessary non-null assertion whenever we use the variable later.
Now, override the onCreate()
method of MusicService
so that it contains the following code:
1 | overridefunonCreate(){ |
2 | super.onCreate() |
3 | songPosition=0 |
4 | mediaPlayer=MediaPlayer() |
5 | |
6 | initMusicPlayer() |
7 | random=Random.Default |
8 | } |
We initialize our mediaPlayer
variable with a new instance of the MediaPlayer
class. The call to our custom initMusicPlayer()
method sets the value of different attributes and properties for this media player.
1 | privatefuninitMusicPlayer(){ |
2 | |
3 | mediaPlayer.setWakeMode( |
4 | applicationContext, |
5 | PowerManager.PARTIAL_WAKE_LOCK) |
6 | |
7 | valaudioAttributes=AudioAttributes.Builder() |
8 | .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) |
9 | .setUsage(AudioAttributes.USAGE_MEDIA) |
10 | .build() |
11 | |
12 | mediaPlayer.setAudioAttributes(audioAttributes) |
13 | |
14 | mediaPlayer.setOnPreparedListener(this) |
15 | mediaPlayer.setOnCompletionListener(this) |
16 | mediaPlayer.setOnErrorListener(this) |
17 | } |
This method does a lot of useful things such as setting the wake mode for the media player to PARTIAL_WAKE_LOCK
. This way, the media player can keep running as the CPU will stay active while the screen can stay off. Then, we create a bunch of audio attributes and assign them to our mediaPlayer
.
The next three lines set the value of different methods for the mediaPlayer
instance to the current class instance. This means that any call to, for example, onPreparedListener()
will be handled by a callback method defined in the current class.
Our songs are stored inside MainActivity
, but we need to pass them to the MusicService
class to play them. Let's define a method inside the MusicService
class to pass the list of songs.
1 | funsetList(theSongs:ArrayList<Song>){ |
2 | songs=theSongs |
3 | } |
We will now define a MusicBinder
class that extends the Binder class. The purpose of this class is to give other classes access to the MusicService
class and its methods from outside the MusicService
class.
1 | classMusicBinder(privatevalservice:MusicService):Binder(){ |
2 | valgetService:MusicService |
3 | get()=service |
4 | } |
Starting the Service
We will now head back to the MainActivity
class and add the following variables to it:
1 | privatevarmusicService:MusicService?=null |
2 | privatevarplayIntent:Intent?=null |
3 | privatevarmusicBound=false |
While the MusicService
class will play the music, the playback will be under the control of the MainActivity
class because that's where the application's user interface is present. Therefore, we will need to bind to the MusicService
class, and we can do so by adding the following code to our class.
1 | privatevalmusicConnection:ServiceConnection=object:ServiceConnection{ |
2 | overridefunonServiceConnected(name:ComponentName,service:IBinder){ |
3 | valbinder=serviceasMusicBinder |
4 | |
5 | musicService=binder.getService |
6 | musicService!!.setList(songs) |
7 | |
8 | musicBound=true |
9 | } |
10 | |
11 | overridefunonServiceDisconnected(name:ComponentName){ |
12 | musicBound=false |
13 | } |
14 | } |
We are using the MusicBinder
class defined in the previous section to get access to the MusicService
class and its methods. That's how we were able to call the setList()
method after initialization within onServiceConnected()
.
Make sure you add the following statement to your imports to avoid any errors.
1 | importcom.tutsplus.musicplayer.MusicService.MusicBinder |
We will now override the onStart()
method of the MainActivity
class to start the MusicService
instance when the MainActivity
instance starts.
1 | overridefunonStart(){ |
2 | super.onStart() |
3 | if(playIntent==null){ |
4 | playIntent=Intent(this,MusicService::class.java) |
5 | bindService(playIntent,musicConnection,BIND_AUTO_CREATE) |
6 | startService(playIntent) |
7 | } |
8 | } |
We begin by checking if an Intent
object exists to start a MusicService
component. A null value means that the service has not yet been started. If no such Intent exists, we create one here. The bindService()
method binds the musicService
to the MainActivity
.
As you can see, we also pass a reference to the ServiceConnection
object in the form of our musicConnection
variable in the second parameter. This handles the connection and binding between our MainActivity
and MusicService
instances.
In the previous section, we created and stored an instance of our MusicBinder
class at the top of the MusicService
class. Add the following two methods to the MusicService
class to handle the binding and unbinding.
1 | overridefunonBind(intent:Intent?):IBinder{ |
2 | returnmusicBinder |
3 | } |
4 | |
5 | overridefunonUnbind(intent:Intent?):Boolean{ |
6 | mediaPlayer.stop() |
7 | mediaPlayer.release() |
8 | returnfalse |
9 | } |
Begin the Playback
The basic setup of the MusicService
and MainActivity
class is now complete. So we can start adding the code that will help us play a song the user selects.
Create a method called playSong()
inside the MusicService
class, and add the following code to it:
1 | funplaySong(){ |
2 | |
3 | mediaPlayer.reset() |
4 | valplaySong=songs[songPosition] |
5 | valcurrentSongId:Long=playSong.id |
6 | |
7 | valtrackUri=ContentUris.withAppendedId( |
8 | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, |
9 | currentSongId |
10 | ) |
11 | |
12 | try{ |
13 | mediaPlayer.setDataSource(applicationContext,trackUri) |
14 | }catch(e:Exception){ |
15 | Log.e("MUSIC SERVICE","Error setting data source",e) |
16 | } |
17 | |
18 | mediaPlayer.prepare() |
19 | } |
We reset our MediaPlayer
instance with each call to the playSong()
method because it is supposed to play multiple songs, and resetting everything takes the player back to its initial state.
Then, we get the current song being played based on the songPosition
and extract its id
from the corresponding Song
object. We create a Uri
based on this id
and then set it as the data source for the mediaPlayer
object.
Finally, we call the prepare()
method to prepare the media player for playback. This is useful when you want to play both local media sources.
Add the following code to the onPrepared()
method of the MusicService
class. This starts the media player once it is ready for playback.
1 | overridefunonPrepared(mediaPlayer:MediaPlayer?){ |
2 | mediaPlayer?.start() |
3 | } |
Also add the following method to the MusicService
class to set the current song. We will be calling this method a bit later.
1 | funsetSong(songIndex:Int){ |
2 | songPosition=songIndex |
3 | } |
The song.xml file that we created in the previous tutorial to specify the layout for individual song items contains the following attribute attached to each item:
1 | android:onClick="songPicked" |
Open MainActivity.kt and add the following method inside it:
1 | funsongPicked(view:View){ |
2 | musicService!!.setSong(view.tag.toString().toInt()) |
3 | musicService!!.playSong() |
4 | } |
When we created our adapter in the previous tutorial, we added the following line to the onBindViewHolder()
method:
1 | songViewHolder.itemView.tag=idx |
This tag
property set as the index of the song is what we are using inside the songPicked()
method to set the song to play.
Update the onDestroy()
method of the MainActivity
to stop the associated MusicService()
when the activity itself is destroyed.
1 | overridefunonDestroy(){ |
2 | stopService(playIntent) |
3 | musicService=null |
4 | |
5 | super.onDestroy() |
6 | } |
Final Thoughts
We have now implemented basic playback of music tracks selected from the user's list of music files. In the final part of this series, we will add a media controller through which the user will be able to control playback. We will add a notification to let the user return to the app after navigating away from it, and we will carry out some housekeeping to make the app cope with a variety of user actions.