If you are just starting out, I recommend reading the first (basics about Media playback on Android) and second (Creating a Service to Play Music in the background) parts before proceeding.

This post will discuss how to show Media Player controls on the lock screen and the notifications.

Music Player controls on Lock Screen

Managing a Media Session

The first thing for this to work is to register a Media Session in our service. A MediaSession should be created to publish media playback information or handle media keys. We create a new MediaSession in the onCreate() of our Service instance.

mSession = new MediaSession(this, "MusicService");
mSession.setCallback(new MediaSessionCallback());
mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);

MediaSession.Callback set with setCallback(Callback) enables us to receive commands, media keys, and other events. Note that we are not calling setActive on the media session yet. We do that only when we receive a play intent for the service and actually start playing some music.

We also need to associate the UI that should be launched when the user wants to interact with this session. Let’s say we have an activity called MusicPlayerActivity that manages a music player interface. This is how we can set it to launch when the user wants to interact with the session.

Context context = getApplicationContext();
Intent intent = new Intent(context, MusicPlayerActivity.class);
PendingIntent pi = PendingIntent.getActivity(context, 99 /*request code*/,
        intent, PendingIntent.FLAG_UPDATE_CURRENT);
mSession.setSessionActivity(pi);

mSessionExtras = new Bundle();
mSession.setExtras(mSessionExtras);

In order to populate the lock screen with the current track information, Media Session uses MediaMetadata. Remember that we created some meta data in the previous post, but there we were using it as just a way to pass on our media ids to the playback implementation. If you have done that, and created a media session, you are already good to go. It will start picking up information from the meta data to show on the lock screen.

If you have the album artwork ready, you can also set it immediately on the meta data using .putBitmap for keys MediaMetadata.METADATA_KEY_ALBUM_ART and MediaMetadata.METADATA_KEY_DISPLAY_ICON.

Loading the album art asynchronously

But, what if you have a url to the album art? Although, MediaMetadata doesn’t directly allow putting url’s for the album arts, it’s pretty easy to load the images asynchronously. Use your favorite image loading library (like Glide, Picasso, Fresco, …) to load the image in the background and then update the meta data when you have the bitmap ready like so.

// Set the proper album artwork on the media session, so it can be shown in the
// locked screen and in other places.
if (track.getDescription().getIconBitmap() == null &&
        track.getDescription().getIconUri() != null) {
    String albumUri = track.getDescription().getIconUri().toString();
    Glide.with(this).load(albumUri).asBitmap().into(new SimpleTarget<Bitmap>() {
        @Override
        public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
            MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
            MediaMetadata track = mMusicProvider.getMusic(trackId);
            track = new MediaMetadata.Builder(track)
                    .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, resource)
                    .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, resource)
                    .build();

            String currentPlayingId = queueItem.getDescription().getMediaId();
            if (trackId.equals(currentPlayingId)) {
                mSession.setMetadata(track);
            }
        }
    });
}

Showing playback controls in notification

Once we have the lock screen controls sorted out, let’s take a look at how we can add these controls to the notification. We always need a notification to make our service act as a foreground service so that it isn’t killed by the system when it needs resources. This is how we can add a notification and set the Service to foreground:

Notification notification = createNotification();
if (notification != null) {
    mController.registerCallback(mCb);
    IntentFilter filter = new IntentFilter();
    filter.addAction(ACTION_NEXT);
    filter.addAction(ACTION_PAUSE);
    filter.addAction(ACTION_PLAY);
    filter.addAction(ACTION_PREV);
    mService.registerReceiver(this, filter);

    mService.startForeground(NOTIFICATION_ID, notification);
    mStarted = true;
}

mController is an instance of MediaController. It is a view containing controls for a MediaPlayer that typically contains the buttons like “Play/Pause”, “Rewind”, “Fast Forward” and a progress slider. It takes care of synchronizing the controls with the state of the MediaPlayer. We use it to simplify the notification creation process to let the system create a “standard” media controller and register a callback (mCb) that listens to the media controller events.

Here’s how we create the notification:

private Notification createNotification() {
    if (mMetadata == null || mPlaybackState == null) {
        return null;
    }

    Notification.Builder notificationBuilder = new Notification.Builder(mService);
    int playPauseButtonPosition = 0;

    // If skip to previous action is enabled
    if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
        notificationBuilder.addAction(R.drawable.ic_prev_gray,
                mService.getString(R.string.label_previous), mPreviousIntent);

        // If there is a "skip to previous" button, the play/pause button will
        // be the second one. We need to keep track of it, because the MediaStyle notification
        // requires to specify the index of the buttons (actions) that should be visible
        // when in compact view.
        playPauseButtonPosition = 1;
    }

    addPlayPauseAction(notificationBuilder);

    // If skip to next action is enabled
    if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
        notificationBuilder.addAction(R.drawable.ic_next_gray,
                mService.getString(R.string.label_next), mNextIntent);
    }

    MediaDescription description = mMetadata.getDescription();

    String fetchArtUrl = null;
    Bitmap art = null;
    if (description.getIconUri() != null) {
        // This sample assumes the iconUri will be a valid URL formatted String, but
        // it can actually be any valid Android Uri formatted String.
        // async fetch the album art icon
        String artUrl = description.getIconUri().toString();
        if (art == null) {
            fetchArtUrl = artUrl;
            // use a placeholder art while the remote art is being downloaded
            art = BitmapFactory.decodeResource(mService.getResources(), R.mipmap.ic_launcher);
        }
    }

    notificationBuilder
            .setStyle(new Notification.MediaStyle()
                .setShowActionsInCompactView(new int[]{playPauseButtonPosition})  // show only play/pause in compact view
                .setMediaSession(mSessionToken))
            .setColor(mNotificationColor)
            .setSmallIcon(R.drawable.ic_notification)
            .setVisibility(Notification.VISIBILITY_PUBLIC)
            .setUsesChronometer(true)
            .setContentIntent(createContentIntent(description)) // Create an intent that would open the UI when user clicks the notification
            .setContentTitle(description.getTitle())
            .setContentText(description.getSubtitle())
            .setLargeIcon(art);

    setNotificationPlaybackState(notificationBuilder);
    if (fetchArtUrl != null) {
        fetchBitmapFromURLAsync(fetchArtUrl, notificationBuilder);
    }

    return notificationBuilder.build();
}

Again, we have some code to fetch the bitmap for the notification on the background thread. I leave it to you to use your favorite image loading library to handle that.

There is some special handling required for the play/pause buttons since we want to show only one of them based on the current state of the media player.

private void addPlayPauseAction(Notification.Builder builder) {
    String label;
    int icon;
    PendingIntent intent;
    if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) {
        label = mService.getString(R.string.label_pause);
        icon = R.drawable.ic_pause_green;
        intent = mPauseIntent;
    } else {
        label = mService.getString(R.string.label_play);
        icon = R.drawable.ic_play_gray;
        intent = mPlayIntent;
    }
    builder.addAction(new Notification.Action(icon, label, intent));
}

And, to update the notification with the current state of the media player

private void setNotificationPlaybackState(Notification.Builder builder) {
    if (mPlaybackState == null || !mStarted) {
        mService.stopForeground(true);
        return;
    }
    if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING
            && mPlaybackState.getPosition() >= 0) {
        builder.setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()).setShowWhen(true).setUsesChronometer(true);
    } else {
        builder.setWhen(0).setShowWhen(false).setUsesChronometer(false);
    }

    // Make sure that the notification can be dismissed by the user when we are not playing:
    builder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING);
}

We need to update the notification whenever we receive some events from the media controller. Here’s how we can create the callback to handle these events:

private final MediaController.Callback mCb = new MediaController.Callback() {
    @Override
    public void onPlaybackStateChanged(@NonNull PlaybackState state) {
        mPlaybackState = state;
        if (state.getState() == PlaybackState.STATE_STOPPED ||
                state.getState() == PlaybackState.STATE_NONE) {
            stopNotification();
        } else {
            Notification notification = createNotification();
            if (notification != null) {
                mNotificationManager.notify(NOTIFICATION_ID, notification);
            }
        }
    }

    @Override
    public void onMetadataChanged(MediaMetadata metadata) {
        mMetadata = metadata;
        Notification notification = createNotification();
        if (notification != null) {
            mNotificationManager.notify(NOTIFICATION_ID, notification);
        }
    }

    @Override
    public void onSessionDestroyed() {
        super.onSessionDestroyed();
        updateSessionToken();
    }
};

Handling user interaction with the controls

So far, we have the controls on the lock screen and the notification. But, they don’t work magically. We will need to listen to user actions on these controls and perform corresponding actions in our Service. Remember that we created the MediaSession and set a callback in the first section. This is where that comes into play. The user events are sent to the media session callback.

Here’s a sample implementation of the MediaSession.Callback that can be used in your Service. You might need to modify it to fit your needs. If you are following this series of tutorials, it should pretty much fit right through in your application.

private final class MediaSessionCallback extends MediaSession.Callback {
    @Override
    public void onPlay() {
        if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
            handlePlayRequest();
        }
    }

    @Override
    public void onSkipToQueueItem(long queueId) {
        if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
            // set the current index on queue from the music Id:
            mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
            // play the music
            handlePlayRequest();
        }
    }

    @Override
    public void onSeekTo(long position) {
        seek(position);
    }

    @Override
    public void onPause() {
        handlePauseRequest();
    }

    @Override
    public void onStop() {
        handleStopRequest(null);
    }

    @Override
    public void onSkipToNext() {
        next();
    }

    @Override
    public void onSkipToPrevious() {
        prev();
    }
}

I have had far too many requests for the source code and the reason I am not putting it up is that as all source code, it requires constant maintenance and I don’t have enough time to manage this. Secondly, I have seen far too many people picking up the source code and putting it as is into the app which floods the already sub-par app market with low-quality apps aimed only for ad revenue. There is already a good, well-maintained music player source code maintained by Google if someone really needs it. Otherwise, if you need professional help, please feel free to shoot me an email.

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