Sapan Diwakar

Software developer

Follow me on Twitter Check out my code on GitHub View some of my designs on Dribbble Take a look at my Linked In profile

Playing music in the background on Android

If you haven't read the first part, read it first to learn the basics about Media playback on Android.

Playing music in the background

If you want your media to play in the background while the user is interacting with other applications, then you must start a Service and control the MediaPlayer instance from there. If you have never worked with Services before, it might be worthwhile to read a quick overview from the guide.

In short, like an Activity, all work in a Service is done in a single thread by default. In fact, if you're running an activity and a service from the same application, they use the same thread (the "main thread") by default. Therefore, services need to process incoming intents quickly and never perform lengthy computations when responding to them. If any heavy work or blocking calls are expected, you must do those tasks asynchronously: either from another thread you implement yourself, or using the framework's many facilities for asynchronous processing.

Running as a foreground service

Services are often used for performing background tasks and in most of these cases, the user is not actively aware of the service's execution, and probably wouldn't even notice if some of these services were interrupted and later restarted. A music playing service on the other hand,
would have the user's continuous attention and would lead to a bad interface if it were to be interrupted. We run our service as a "foreground service" to signal to the system that it is a high priority service. When running in the foreground, the service must provide a status bar notification to ensure that users are aware of the running service and allow them to open an activity that can interact with the service.

In order to turn your service into a foreground service, you must create a Notification for the status bar and call startForeground() from the Service.

Playing music

If you followed my previous post, you already know how to play music using a MediaPlayer instance. Now, we just need to hook everything up in the service using our LocalPlayback implementation. First, we can create a playback instance in onCreate

mPlayback = new LocalPlayback(this, mMusicProvider);  
mPlayback.setState(PlaybackState.STATE_NONE);  
mPlayback.setCallback(this);  
mPlayback.start();  

onStartCommand() is called by the system every time a client explicitly starts the service by calling startService(Intent), providing the arguments it supplied and a unique integer token representing the start request. We will read this to handle the commands passed from UI parts of our application to start/stop playback using the Intent action and a String extra command. When we want to play some tracks, the UI passes it as an extra ARG_QUEUE and the index in extra ARG_INDEX.

@Override
public int onStartCommand(Intent startIntent, int flags, int startId) {  
   if (startIntent != null) {
       String action = startIntent.getAction();
       String command = startIntent.getStringExtra(CMD_NAME);
       if (ACTION_CMD.equals(action)) {
           if (CMD_PAUSE.equals(command)) {
               if (mPlayback != null && mPlayback.isPlaying()) {
                   handlePauseRequest();
               }
           } else if (CMD_PLAY.equals(command)) {
               ArrayList<Track> queue = new ArrayList<>();
               for (Parcelable input : startIntent.getParcelableArrayListExtra(ARG_QUEUE)) {
                   queue.add((Track) Parcels.unwrap(input));
               }
               int index = startIntent.getIntExtra(ARG_INDEX, 0);
               playWithQueue(queue, index);
           }
       }
   }

   return START_STICKY;
}

Let's see how we can create the intent to play some music:

Intent intent = new Intent(MusicService.ACTION_CMD, fileUrlToPlay, activity, MusicService::class.java)  
intent.putParcelableArrayListExtra(MusicService.ARG_QUEUE, tracks)  
intent.putExtra(MusicService.ARG_INDEX, position)  
intent.putExtra(MusicService.CMD_NAME, MusicService.CMD_PLAY)  
activity.startService(intent)  

Handling play/pause/stop commands

With most of the logic handling out of the way, we just have to put everything together with some boilerplate code. I am intentionally leaving simple stuff out to keep the post small and readable. If you have any questions, feel free to write me an email.

public void playWithQueue(List<Track> queue, int index) {  
    if (index < 0 || index >= queue.size()) {
        return;
    }
    this.mPlayingQueue = queueToMediaItems(queue);
    mCurrentIndexOnQueue = index;
    handlePlayRequest();
}

private void handlePlayRequest() {  
    play(false);
}

private void handlePauseRequest() {  
    mPlayback.pause();
}

private void handleStopRequest(String withError) {  
    mPlayback.stop(true);
    updatePlaybackState(withError);

    // Service is no longer necessary. Will be started again if needed.
    stopSelf();
    mServiceStarted = false;
}

public void play(boolean reset) {  
    mDelayedStopHandler.removeCallbacksAndMessages(null);
    if (!mServiceStarted) {
        LogHelper.v(TAG, "Starting service");
        // The MusicService needs to keep running even after the calling MediaBrowser
        // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
        // need to play media.
        startService(new Intent(getApplicationContext(), MusicService.class));
        mServiceStarted = true;
    }

    if (!mSession.isActive()) {
        mSession.setActive(true);
    }

    if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
        updateMetadata();
        mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue), reset);
    }
}

private void updateMetadata() {  
    if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
        LogHelper.e(TAG, "Can't retrieve current metadata.");
        updatePlaybackState(getResources().getString(R.string.error_no_metadata));
        return;
    }
    MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
    String musicId = queueItem.getDescription().getMediaId();
    MediaMetadata track = mMusicProvider.getMusic(musicId);
    if (track == null) {
        throw new IllegalArgumentException("Invalid musicId " + musicId);
    }
    final String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
    if (!TextUtils.equals(musicId, trackId)) {
        IllegalStateException e = new IllegalStateException("track ID should match musicId.");
        throw e;
    }
    mSession.setMetadata(track);
}

Bonus

MediaPlayer consumes a considerable amount of device resources (read battery). Since we are set up as a foreground service, the system would almost never kill it automatically. This leaves with two options - 1. The user kills it after he is done playing music. 2. If there is no music playing for some time, we should stop the service to stop consuming resources. Let's see how we can set a handler to terminate the Service automatically after music stops.

private static class DelayedStopHandler extends Handler {  
  private final WeakReference<MusicService> mWeakReference;

  private DelayedStopHandler(MusicService service) {
      mWeakReference = new WeakReference<>(service);
  }

  @Override
  public void handleMessage(Message msg) {
      MusicService service = mWeakReference.get();
      if (service != null && service.mPlayback != null) {
          if (service.mPlayback.isPlaying()) {
              Timber.d("Ignoring delayed stop since the media player is in use.");
              return;
          }
          Timber.d("Stopping service with delay handler.");
          service.stopSelf();
          service.mServiceStarted = false;
      }
  }
}

This Handler takes care of stopping the service if it's not playing. Now, we need to set this up so it fires up after we start playing. Put this at the end of onStartCommand(), pause() and handleStopRequest().

// Reset the delay handler to enqueue a message to stop the service if
// nothing is playing.
mDelayedStopHandler.removeCallbacksAndMessages(null);  
mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);  

That's all for this post. We discussed on how to create a background service to play music using our playback interface created in the previous tutorial. In the next part, we will try to set up the music player UI on the lock screen and notification drawer.

This is the second tutorial in the series Building a Music Player App for Android. Stay tuned for more tutorials in this series! Take me to next tutorial