Android Lollipop is brimming with new features, and one of them is lock screen notifications. Up until this point, lock screen media controls had to be implemented through the use of a RemoteView
, and media control notifications had to be built with custom views. In this tutorial I will go over using the new MediaStyle
for notifications and interacting with a MediaSession
for controlling media playback states. All code for this tutorial can be found on GitHub.
The very first thing that we're going to need to do is set the MEDIA_CONTENT_CONTROL
permission in AndroidManifest.xml
. This will allow us to use our lock screen notification to control media.
android:name="android.permission.MEDIA_CONTENT_CONTROL" />
For this example we're going to use a background service to controls our media and build/interact with the new notifications. Our MainActivity
is going to be very straight forward and simply start our service with an action telling the service to build a playing state notification.
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( MediaPlayerService.ACTION_PLAY );
startService( intent );
Next, we're going to want to start fleshing out MediaPlayerService
. At the top of the class we're going to define a set of strings that we will use to implement notification actions, and also define the objects that we'll use throughout the class.
public static final String ACTION_PLAY = "action_play";
public static final String ACTION_PAUSE = "action_pause";
public static final String ACTION_REWIND = "action_rewind";
public static final String ACTION_FAST_FORWARD = "action_fast_foward";
public static final String ACTION_NEXT = "action_next";
public static final String ACTION_PREVIOUS = "action_previous";
public static final String ACTION_STOP = "action_stop";
private MediaPlayer mMediaPlayer;
private MediaSessionManager mManager;
private MediaSession mSession;
private MediaController mController;
When the service receives an intent, it'll immediately go through onStartCommand
. This method only does two simple things: if our objects have not been initialized, it'll call initMediaSession
to set them up, and then the intent will be passed to handleIntent
.
initMediaSession
initializes the objects that we defined earlier. MediaPlayer
handles media playback (not used in this example, but in an actual app it would be), the MediaSessionManager
helps maintain the MediaSession
, the MediaSession
itself is used for keeping track of media states, and the MediaController
handles transitioning between media states and callingMediaPlayer
methods.
mMediaPlayer = new MediaPlayer();
mManager = (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE);
mSession = mManager.createSession("sample session");
mController = MediaController.fromToken( mSession.getSessionToken() );
The next thing initMediaSession
does is add TransportControlCallbacks
that we can call in order to control the MediaPlayer
and display new notifications.
mSession.addTransportControlsCallback( new MediaSession.TransportControlsCallback() {
@Override
public void onPlay() {
super.onPlay();
Log.e( "MediaPlayerService", "onPlay");
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onPause() {
super.onPause();
Log.e( "MediaPlayerService", "onPause");
buildNotification(generateAction(android.R.drawable.ic_media_play, "Play", ACTION_PLAY));
}
@Override
public void onSkipToNext() {
super.onSkipToNext();
Log.e( "MediaPlayerService", "onSkipToNext");
//Change media here
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
Log.e( "MediaPlayerService", "onSkipToPrevious");
//Change media here
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onFastForward() {
super.onFastForward();
Log.e( "MediaPlayerService", "onFastForward");
//Manipulate current media here
}
@Override
public void onRewind() {
super.onRewind();
Log.e( "MediaPlayerService", "onRewind");
//Manipulate current media here
}
@Override
public void onStop() {
super.onStop();
Log.e( "MediaPlayerService", "onStop");
//Stop media player here
NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel( 1 );
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
stopService( intent );
}
@Override
public void onSeekTo(long pos) {
super.onSeekTo(pos);
}
@Override
public void onSetRating(Rating rating) {
super.onSetRating(rating);
}
});
These control methods are called in handleIntent
. When an intent is received by the service,handleIntent
extracts the action associated with that intent to determine which transport control method should be called.
private void handleIntent( Intent intent ) {
if( intent == null || intent.getAction() == null )
return;
String action = intent.getAction();
if( action.equalsIgnoreCase( ACTION_PLAY ) ) {
mController.getTransportControls().play();
} else if( action.equalsIgnoreCase( ACTION_PAUSE ) ) {
mController.getTransportControls().pause();
} else if( action.equalsIgnoreCase( ACTION_FAST_FORWARD ) ) {
mController.getTransportControls().fastForward();
} else if( action.equalsIgnoreCase( ACTION_REWIND ) ) {
mController.getTransportControls().rewind();
} else if( action.equalsIgnoreCase( ACTION_PREVIOUS ) ) {
mController.getTransportControls().skipToPrevious();
} else if( action.equalsIgnoreCase( ACTION_NEXT ) ) {
mController.getTransportControls().skipToNext();
} else if( action.equalsIgnoreCase( ACTION_STOP ) ) {
mController.getTransportControls().stop();
}
}
As can be seen in the TransportControlCallbacks
, we call buildNotification
with another method called generateAction
. Actions are used by the MediaStyle
notification to populate the buttons at the bottom of the notification and launch intents when pressed. generateAction
simply accepts the icon that the notification will use for that button, a title for the button and a string that will be used as the action identifier in handleIntent. With this information, generateAction
is able to construct a pendingIntent
before assigning it to an action that is then returned.
private Notification.Action generateAction( int icon, String title, String intentAction ) {
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( intentAction );
PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
return new Notification.Action.Builder( icon, title, pendingIntent ).build();
}
buildNotification
is where we actually implement the MediaStyle
notification. The first thing we do is create a new Notification.MediaStyle
style, then start building the rest of the notification through using Notification.Builder
. This notification simply contains apendingIntent
that would stop our media when the notification is dismissed, a title, content text, a small icon and the style.
private void buildNotification( Notification.Action action ) {
Notification.MediaStyle style = new Notification.MediaStyle();
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( ACTION_STOP );
PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
Notification.Builder builder = new Notification.Builder( this )
.setSmallIcon( R.drawable.ic_launcher )
.setContentTitle( "Media Title" )
.setContentText( "Media Artist" )
.setDeleteIntent( pendingIntent )
.setStyle( style );
Next we can add our buttons to the notification through the use of Builder.addAction
. It should be noted that MediaStyle
notifications only support up to five different actions. Each of our actions will be generated through the generateAction
method described above.
builder.addAction( generateAction( android.R.drawable.ic_media_previous, "Previous", ACTION_PREVIOUS ) );
builder.addAction( generateAction( android.R.drawable.ic_media_rew, "Rewind", ACTION_REWIND ) );
builder.addAction( action );
builder.addAction( generateAction( android.R.drawable.ic_media_ff, "Fast Foward", ACTION_FAST_FORWARD ) );
builder.addAction( generateAction( android.R.drawable.ic_media_next, "Next", ACTION_NEXT ) );
The last thing this method does is actually construct the notification and post it to the system
NotificationManager notificationManager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
notificationManager.notify( 1, builder.build() );
Now that the notification and sessions are implementing and working, the final thing we need to take into account is releasing our MediaSession
once the media player and service have been stopped.
@Override
public boolean onUnbind(Intent intent) {
mSession.release();
return super.onUnbind(intent);
}
And with that we now have a fully working MediaStyle
notification on our lock screen and in the notification drawer that takes advantage of MediaSession
for playback control. Enjoy!
Android Lollipop is brimming with new features, and one of them is lock screen notifications. Up until this point, lock screen media controls had to be implemented through the use of a RemoteView
, and media control notifications had to be built with custom views. In this tutorial I will go over using the new MediaStyle
for notifications and interacting with a MediaSession
for controlling media playback states. All code for this tutorial can be found on GitHub.
The very first thing that we're going to need to do is set the MEDIA_CONTENT_CONTROL
permission in AndroidManifest.xml
. This will allow us to use our lock screen notification to control media.
android:name="android.permission.MEDIA_CONTENT_CONTROL" />
For this example we're going to use a background service to controls our media and build/interact with the new notifications. Our MainActivity
is going to be very straight forward and simply start our service with an action telling the service to build a playing state notification.
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( MediaPlayerService.ACTION_PLAY );
startService( intent );
Next, we're going to want to start fleshing out MediaPlayerService
. At the top of the class we're going to define a set of strings that we will use to implement notification actions, and also define the objects that we'll use throughout the class.
public static final String ACTION_PLAY = "action_play";
public static final String ACTION_PAUSE = "action_pause";
public static final String ACTION_REWIND = "action_rewind";
public static final String ACTION_FAST_FORWARD = "action_fast_foward";
public static final String ACTION_NEXT = "action_next";
public static final String ACTION_PREVIOUS = "action_previous";
public static final String ACTION_STOP = "action_stop";
private MediaPlayer mMediaPlayer;
private MediaSessionManager mManager;
private MediaSession mSession;
private MediaController mController;
When the service receives an intent, it'll immediately go through onStartCommand
. This method only does two simple things: if our objects have not been initialized, it'll call initMediaSession
to set them up, and then the intent will be passed to handleIntent
.
initMediaSession
initializes the objects that we defined earlier. MediaPlayer
handles media playback (not used in this example, but in an actual app it would be), the MediaSessionManager
helps maintain the MediaSession
, the MediaSession
itself is used for keeping track of media states, and the MediaController
handles transitioning between media states and callingMediaPlayer
methods.
mMediaPlayer = new MediaPlayer();
mManager = (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE);
mSession = mManager.createSession("sample session");
mController = MediaController.fromToken( mSession.getSessionToken() );
The next thing initMediaSession
does is add TransportControlCallbacks
that we can call in order to control the MediaPlayer
and display new notifications.
mSession.addTransportControlsCallback( new MediaSession.TransportControlsCallback() {
@Override
public void onPlay() {
super.onPlay();
Log.e( "MediaPlayerService", "onPlay");
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onPause() {
super.onPause();
Log.e( "MediaPlayerService", "onPause");
buildNotification(generateAction(android.R.drawable.ic_media_play, "Play", ACTION_PLAY));
}
@Override
public void onSkipToNext() {
super.onSkipToNext();
Log.e( "MediaPlayerService", "onSkipToNext");
//Change media here
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
Log.e( "MediaPlayerService", "onSkipToPrevious");
//Change media here
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onFastForward() {
super.onFastForward();
Log.e( "MediaPlayerService", "onFastForward");
//Manipulate current media here
}
@Override
public void onRewind() {
super.onRewind();
Log.e( "MediaPlayerService", "onRewind");
//Manipulate current media here
}
@Override
public void onStop() {
super.onStop();
Log.e( "MediaPlayerService", "onStop");
//Stop media player here
NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel( 1 );
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
stopService( intent );
}
@Override
public void onSeekTo(long pos) {
super.onSeekTo(pos);
}
@Override
public void onSetRating(Rating rating) {
super.onSetRating(rating);
}
});
These control methods are called in handleIntent
. When an intent is received by the service,handleIntent
extracts the action associated with that intent to determine which transport control method should be called.
private void handleIntent( Intent intent ) {
if( intent == null || intent.getAction() == null )
return;
String action = intent.getAction();
if( action.equalsIgnoreCase( ACTION_PLAY ) ) {
mController.getTransportControls().play();
} else if( action.equalsIgnoreCase( ACTION_PAUSE ) ) {
mController.getTransportControls().pause();
} else if( action.equalsIgnoreCase( ACTION_FAST_FORWARD ) ) {
mController.getTransportControls().fastForward();
} else if( action.equalsIgnoreCase( ACTION_REWIND ) ) {
mController.getTransportControls().rewind();
} else if( action.equalsIgnoreCase( ACTION_PREVIOUS ) ) {
mController.getTransportControls().skipToPrevious();
} else if( action.equalsIgnoreCase( ACTION_NEXT ) ) {
mController.getTransportControls().skipToNext();
} else if( action.equalsIgnoreCase( ACTION_STOP ) ) {
mController.getTransportControls().stop();
}
}
As can be seen in the TransportControlCallbacks
, we call buildNotification
with another method called generateAction
. Actions are used by the MediaStyle
notification to populate the buttons at the bottom of the notification and launch intents when pressed. generateAction
simply accepts the icon that the notification will use for that button, a title for the button and a string that will be used as the action identifier in handleIntent. With this information, generateAction
is able to construct a pendingIntent
before assigning it to an action that is then returned.
private Notification.Action generateAction( int icon, String title, String intentAction ) {
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( intentAction );
PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
return new Notification.Action.Builder( icon, title, pendingIntent ).build();
}
buildNotification
is where we actually implement the MediaStyle
notification. The first thing we do is create a new Notification.MediaStyle
style, then start building the rest of the notification through using Notification.Builder
. This notification simply contains apendingIntent
that would stop our media when the notification is dismissed, a title, content text, a small icon and the style.
private void buildNotification( Notification.Action action ) {
Notification.MediaStyle style = new Notification.MediaStyle();
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( ACTION_STOP );
PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
Notification.Builder builder = new Notification.Builder( this )
.setSmallIcon( R.drawable.ic_launcher )
.setContentTitle( "Media Title" )
.setContentText( "Media Artist" )
.setDeleteIntent( pendingIntent )
.setStyle( style );
Next we can add our buttons to the notification through the use of Builder.addAction
. It should be noted that MediaStyle
notifications only support up to five different actions. Each of our actions will be generated through the generateAction
method described above.
builder.addAction( generateAction( android.R.drawable.ic_media_previous, "Previous", ACTION_PREVIOUS ) );
builder.addAction( generateAction( android.R.drawable.ic_media_rew, "Rewind", ACTION_REWIND ) );
builder.addAction( action );
builder.addAction( generateAction( android.R.drawable.ic_media_ff, "Fast Foward", ACTION_FAST_FORWARD ) );
builder.addAction( generateAction( android.R.drawable.ic_media_next, "Next", ACTION_NEXT ) );
The last thing this method does is actually construct the notification and post it to the system
NotificationManager notificationManager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
notificationManager.notify( 1, builder.build() );
Now that the notification and sessions are implementing and working, the final thing we need to take into account is releasing our MediaSession
once the media player and service have been stopped.
@Override
public boolean onUnbind(Intent intent) {
mSession.release();
return super.onUnbind(intent);
}
And with that we now have a fully working MediaStyle
notification on our lock screen and in the notification drawer that takes advantage of MediaSession
for playback control. Enjoy!