Advertisement

Create a Music Player on Android: User Controls

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

We are building a simple music player app for Android in this series. So far, we have presented a list of the songs on the device and allowed the user to make selections from it, starting playback using the MediaPlayer class in a Service class. In this final part of the series, we will let the user control playback, including skipping to the next and previous tracks, fast-forwarding, rewinding, playing, pausing, and seeking to particular points in the track. We will also display a notification during playback so that the user can jump back to the music player after using other apps.

Introduction

The music player control functionality will be implemented using the MediaController class, in which a SeekBar instance displays the progress of playback as well as letting the user skip to particular locations in a track. We will use the Notification and PendingIntent classes to display the title of the currently playing track and let the user navigate back to the app.

This is how the app should look when you complete this tutorial:

Android Music Player

After this series we will also explore related functionality you may wish to use to enhance the music player app. This will include video playback, streaming media, managing audio focus, and presenting media data in different ways.

1. Create a Controller

Step 1

Open your main Activity class and add the following import statement:

import android.widget.MediaController.MediaPlayerControl;

Extend the opening line of the class declaration as follows, so that we can use the Activity class to provide playback control:

public class MainActivity extends Activity implements MediaPlayerControl {

Hover over the class name and select Add unimplemented methods. Eclipse will add various methods for playback control, which we will tailor as we go along.

Step 2

The MediaController class presents a standard widget with play/pause, rewind, fast-forward, and skip (previous/next) buttons in it. The widget also contains a seek bar, which updates as the song plays and contains text indicating the duration of the song and the player's current position. So that we can configure the details of the control, we will implement a class to extend it. Add a new class to your project, naming it MusicController. In Eclipse, choose android.widget.MediaController as the superclass when creating it.

Give the class the following content:

public class MusicController extends MediaController {

  public MusicController(Context c){
    super(c);
  }

  public void hide(){}

}

You can tailor the MediaController class in various ways. All we want to do is stop it from automatically hiding after three seconds by overriding the hide method.

Tip: You may need to tweak the theme your app uses in order to ensure that the media controller text is clearly visible.

Step 3

Back in your main Activity class, add a new instance variable:

private MusicController controller;

We will be setting the controller up more than once in the life cycle of the app, so let's do it in a helper method. Add the following code snippet to your Activity class:

private void setController(){
  //set the controller up
}

Inside the method, instantiate the controller:

controller = new MusicController(this);

You can configure various aspects of the MediaController instance. For example, we will need to determine what will happen when the user presses the previous/next buttons. After instantiating the controller set these click listeners:

controller.setPrevNextListeners(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    playNext();
  }
}, new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    playPrev();
  }
});

We will implement playNext and playPrev a bit later, so just ignore the errors for now. Still inside the setController method, set the controller to work on media playback in the app, with its anchor view referring to the list we included in the layout:

controller.setMediaPlayer(this);
controller.setAnchorView(findViewById(R.id.song_list));
controller.setEnabled(true);

Back in onCreate, call the method:

setController();

We will also call it elsewhere in the class later.

2. Implement Playback Control

Step 1

Remember that the media playback is happening in the Service class, but that the user interface comes from the Activity class. In the previous tutorial, we bound the Activity instance to the Service instance, so that we could control playback from the user interface. The methods in our Activity class that we added to implement the MediaPlayerControl interface will be called when the user attempts to control playback. We will need the Service class to act on this control, so open your Service class now to add a few more methods to it:

public int getPosn(){
  return player.getCurrentPosition();
}

public int getDur(){
  return player.getDuration();
}

public boolean isPng(){
  return player.isPlaying();
}

public void pausePlayer(){
  player.pause();
}

public void seek(int posn){
  player.seekTo(posn);
}

public void go(){
  player.start();
}

These methods all apply to standard playback control functions that the user will expect.

Step 2

Now let's add methods to the Service class for skipping to the next and previous tracks. Start with the previous function:

public void playPrev(){
  songPosn--;
  if(songPosn<0) songPosn=songs.size()-1;
  playSong();
}

We decrement the song index variable, check that we haven't gone outside the range of the list, and call the playSong method we added. Now add the method to skip to the next track:

//skip to next
public void playNext(){
  songPosn++;
  if(songPosn>=songs.size()) songPosn=0;
  playSong();
}

This is analogous to the method for playing the previous track at the moment, but we will amend this method later to implement the shuffle functionality.

Step 3

Now switch back to your Activity class so that we can make use of these methods. First add the methods we called when we set the controller up:

//play next
private void playNext(){
  musicSrv.playNext();
  controller.show(0);
}

//play previous
private void playPrev(){
  musicSrv.playPrev();
  controller.show(0);
}

We call the methods we added to the Service class. We will be adding more code to these later to take care of particular situations. Now let's turn to the MediaPlayerControl interface methods, which will be called by the system during playback and when the user interacts with the controls. These methods should already be in your Activity class, so we will just be altering their implementation.

Start with the canPause method, setting it to true:

@Override
public boolean canPause() {
  return true;
}

Now do the same for the canSeekBackward and canSeekForward methods:

@Override
public boolean canSeekBackward() {
  return true;
}

@Override
public boolean canSeekForward() {
  return true;
}

You can leave the getAudioSessionId and getBufferPercentage methods as they are. Amend the getCurrentPosition method as follows:

@Override
public int getCurrentPosition() {
  if(musicSrv!=null && musicBound && musicSrv.isPng())
    return musicSrv.getPosn();
  else return 0;
}

The conditional tests are to avoid various exceptions that may occur when using the MediaPlayer and MediaController classes. If you attempt to enhance the app in any way, you will likely find that you need to take such steps since the media playback classes throw lots of exceptions. Notice that we call the getPosn method of the Service class.

Amend the getDuration method similarly:

@Override
public int getDuration() {
  if(musicSrv!=null && musicBound && musicSrv.isPng())
    return musicSrv.getDur();
  else return 0;
}

Alter the isPlaying method by invoking the isPng method of our Service class:

@Override
public boolean isPlaying() {
  if(musicSrv!=null && musicBound)
    return musicSrv.isPng();
  return false;
}

Do the same for the pause, seekTo and start methods:

@Override
public void pause() {
  musicSrv.pausePlayer();
}

@Override
public void seekTo(int pos) {
  musicSrv.seek(pos);
}

@Override
public void start() {
  musicSrv.go();
}

3. Handle Navigation Back Into the App

Step 1

Remember that we are going to continue playback even when the user navigates away from the app. In order to facilitate this, we will display a notification showing the title of the track being played. Clicking the notification will take the user back into the app. Switch back to your Service class and add the following additional imports:

import java.util.Random;
import android.app.Notification;
import android.app.PendingIntent;

Now move to the onPrepared method, in which we currently simply start the playback. After the call to player.start(), add the following code:

Intent notIntent = new Intent(this, MainActivity.class);
notIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendInt = PendingIntent.getActivity(this, 0,
  notIntent, PendingIntent.FLAG_UPDATE_CURRENT);

Notification.Builder builder = new Notification.Builder(this);

builder.setContentIntent(pendInt)
  .setSmallIcon(R.drawable.play)
  .setTicker(songTitle)
  .setOngoing(true)
  .setContentTitle("Playing")
  .setContentText(songTitle);
Notification not = builder.build();

startForeground(NOTIFY_ID, not);

We will add the missing variables next. The PendingIntent class will take the user back to the main Activity class when they select the notification. Add variables for the song title and notification ID at the top of the class:

private String songTitle="";
private static final int NOTIFY_ID=1;

Now we need to set the song title, in the playSong method, after the line in which we retrieve the song from the list (Song playSong = songs.get(songPosn);):

songTitle=playSong.getTitle();

Since we have called setForeground on the notification, we need to make sure we stop it when the Service instance is destroyed. Override the following method:

@Override
public void onDestroy() {
  stopForeground(true);
}

4. Shuffle Playback

Step 1

Remember that we added a shuffle button, so let's implement that now. First add new instance variables to the Service class:

private boolean shuffle=false;
private Random rand;

Instantiate the random number generator in onCreate:

rand=new Random();

Now add a method to set the shuffle flag:

public void setShuffle(){
  if(shuffle) shuffle=false;
  else shuffle=true;
}

We will simply toggle the shuffle setting on and off. We will check this flag when the user either skips to the next track or when a track ends and the next one begins. Amend the playNext method as follows:

public void playNext(){
  if(shuffle){
    int newSong = songPosn;
    while(newSong==songPosn){
      newSong=rand.nextInt(songs.size());
    }
    songPosn=newSong;
  }
  else{
    songPosn++;
    if(songPosn>=songs.size()) songPosn=0;
  }
  playSong();
}

If the shuffle flag is on, we choose a new song from the list at random, making sure we don't repeat the last song played. You could enhance this functionality by using a queue of songs and preventing any song from being repeated until all songs have been played.

Step 2

Now we can let the user select the shuffle function. Back in your main Activity class in the onOptionsItemSelected method, amend the section for the shuffle action to call the new method we added to the Service class:

case R.id.action_shuffle:
  musicSrv.setShuffle();
  break;

Now the user will be able to use the menu item to toggle the shuffling functionality.

5. Tidy Up

Step 1

We are almost done, but still need to add a few bits of processing to take care of certain changes, such as the user leaving the app or pausing playback. In your Activity class, add a couple more instance variables:

private boolean paused=false, playbackPaused=false;

We will use these to cope with the user returning to the app after leaving it and interacting with the controls when playback itself is paused. Override onPause to set one of these flags:

@Override
protected void onPause(){
  super.onPause();
  paused=true;
}

Now override onResume:

@Override
protected void onResume(){
  super.onResume();
  if(paused){
    setController();
    paused=false;
  }
}

This will ensure that the controller displays when the user returns to the app. Override onStop to hide it:

@Override
protected void onStop() {
  controller.hide();
  super.onStop();
}

Step 2

If the user interacts with the controls while playback is paused, the MediaPlayer object may behave unpredictably. To cope with this, we will set and use the playbackPaused flag. First amend the playNext and playPrev methods:

private void playNext(){
  musicSrv.playNext();
  if(playbackPaused){
    setController();
    playbackPaused=false;
  }
  controller.show(0);
}

private void playPrev(){
  musicSrv.playPrev();
  if(playbackPaused){
    setController();
    playbackPaused=false;
  }
  controller.show(0);
}

We reset the controller and update the playbackPaused flag when playback has been paused. Now make similar changes to the playSong method:

public void songPicked(View view){
  musicSrv.setSong(Integer.parseInt(view.getTag().toString()));
  musicSrv.playSong();
  if(playbackPaused){
    setController();
    playbackPaused=false;
  }
  controller.show(0);
}

Now set playbackPaused to true in the pause method:

@Override
public void pause() {
  playbackPaused=true;
  musicSrv.pausePlayer();
}

As you work with the MediaPlayer and MediaController classes, you will find that this type of processing is a necessary requirement to avoid errors. For example, you will sometimes find that the controller's seek bar does not update until the user interacts with it. These resources behave differently on different API levels, so thorough testing and tweaking is essential if you plan on releasing your app to the public. The app we are creating in this series is really only a foundation.

Step 3

Let's take some final steps to make the app behave consistently. Back in the Service class, amend the onError method:

@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
  mp.reset();
  return false;
}

We simply reset the player, but you may of course wish to enhance this approach.

The onCompletion method will fire when a track ends, including cases where the user has chosen a new track or skipped to the next/previous tracks as well as when the track reaches the end of its playback. In the latter case, we want to continue playback by playing the next track. To do this we need to check the state of playback. Amend your onCompletion method:

@Override
public void onCompletion(MediaPlayer mp) {
  if(player.getCurrentPosition()>0){
    mp.reset();
    playNext();
  }
}

We call the playNext method if the current track has reached its end.

Tip: To ensure that your app does not interfere with other audio services on the user's device, you should enhance it to handle audio focus gracefully. Make the Service class implement the AudioManager.OnAudioFocusChangeListener interface. In the onCreate method, create an instance of the AudioManager class and call requestAudioFocus on it. Finally, implement the onAudioFocusChange method in your class to control what should happen when the application gains or loses audio focus. See the Audio Focus section in the Developer Guide for more details.

That is the basic app complete! However, you may well need to carry out additional enhancements to make it function reliably across user devices and API levels. The controls should appear whenever you interact with the app.

Android Music Player

The notification should allow you to return to the app while playback continues.

Android Music Player

Conclusion

We have now completed the basic music player for Android. There are many ways in which you could enhance the app, such as adding support for streaming media, video, audio focus, and providing different methods to interact with the music tracks on the device. We will look at some of these enhancements in future tutorials, outlining how you can add them to the app or to other media playback projects. In the meantime, see if you can extend the app to build additional functionality or to improve reliability on different devices. See the Media Playback section of the Android Developer Guide for more information.

Advertisement