One of the most popular uses for mobile devices is playing back audio through music streaming services, downloaded podcasts, or any other number of audio sources. While this is a fairly common feature, it's hard to implement, with lots of different pieces that need to be built correctly in order to give your user the full Android experience.
In this tutorial you will learn about MediaSessionCompat
from the Android support library, and how it can be used to create a proper background audio service for your users.
Setup
The first thing you will need to do is include the Android support library into your project. This can be done by adding the following line into your module's build.gradle file under the dependencies node.
compile 'com.android.support:support-v13:24.2.1'
After you have synced your project, create a new Java class. For this example I will call the class BackgroundAudioService
. This class will need to extend MediaBrowserServiceCompat
. We will also implement the following interfaces: MediaPlayer.OnCompletionListener
and AudioManager.OnAudioFocusChangeListener
.
Now that your MediaBrowserServiceCompat
implementation is created, let's take a moment to update AndroidManifest.xml before returning to this class. At the top of the class, you will need to request the WAKE_LOCK
permission.
<uses-permission android:name="android.permission.WAKE_LOCK" />
Next, within the application
node, declare your new service with the following intent-filter
items. These will allow your service to intercept control buttons, headphone events and media browsing for devices, such as Android Auto (although we won't do anything with Android Auto for this tutorial, some basic support for it is still required by MediaBrowserServiceCompat
).
<service android:name=".BackgroundAudioService"> <intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.media.AUDIO_BECOMING_NOISY" /> <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter> </service>
Finally, you will need to declare the use of the MediaButtonReceiver
from the Android support library. This will allow you to intercept media control button interactions and headphone events on devices running KitKat and earlier.
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver"> <intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.media.AUDIO_BECOMING_NOISY" /> </intent-filter> </receiver>
Now that your AndroidManifest.xml file is finished, you can close it. We're also going to create another class named MediaStyleHelper
, which was written by Ian Lake, Developer Advocate at Google, to clean up the creation of media style notifications.
public class MediaStyleHelper { /** * Build a notification using the information from the given media session. Makes heavy use * of {@link MediaMetadataCompat#getDescription()} to extract the appropriate information. * @param context Context used to construct the notification. * @param mediaSession Media session to get information. * @return A pre-built notification with information from the given media session. */ public static NotificationCompat.Builder from( Context context, MediaSessionCompat mediaSession) { MediaControllerCompat controller = mediaSession.getController(); MediaMetadataCompat mediaMetadata = controller.getMetadata(); MediaDescriptionCompat description = mediaMetadata.getDescription(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context); builder .setContentTitle(description.getTitle()) .setContentText(description.getSubtitle()) .setSubText(description.getDescription()) .setLargeIcon(description.getIconBitmap()) .setContentIntent(controller.getSessionActivity()) .setDeleteIntent( MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP)) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); return builder; } }
Once that's created, go ahead and close the file. We will focus on the background audio service in the next section.
Building Out the Background Audio Service
Now it's time to dig into the core of creating your media app. There are a few member variables that you will want to declare first for this sample app: a MediaPlayer
for the actual playback, and a MediaSessionCompat
object that will manage metadata and playback controls/states.
private MediaPlayer mMediaPlayer; private MediaSessionCompat mMediaSessionCompat;
In addition, you will need a BroadcastReceiver
that listens for changes in the headphone state. To keep things simple, this receiver will pause the MediaPlayer
, if it is playing.
private BroadcastReceiver mNoisyReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if( mMediaPlayer != null && mMediaPlayer.isPlaying() ) { mMediaPlayer.pause(); } } };
For the final member variable, you will to create a MediaSessionCompat.Callback
object, which is used for handling playback state when media session actions occur.
private MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() { @Override public void onPlay() { super.onPlay(); } @Override public void onPause() { super.onPause(); } @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { super.onPlayFromMediaId(mediaId, extras); } };
We will revisit each of the above methods later in this tutorial, as they will be used to drive operations in our media app.
There are two methods that we will also need to declare, though they won't need to do anything for the purposes of this tutorial: onGetRoot()
and onLoadChildren()
. You can use the following code for your defaults.
@Nullable @Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) { if(TextUtils.equals(clientPackageName, getPackageName())) { return new BrowserRoot(getString(R.string.app_name), null); } return null; } //Not important for general audio service, required for class @Override public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) { result.sendResult(null); }
Lastly, you will want to override the onStartCommand()
method, which is the entry point into your Service
. This method will take the Intent that is passed to the Service
and send it to the MediaButtonReceiver
class.
@Override public int onStartCommand(Intent intent, int flags, int startId) { MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent); return super.onStartCommand(intent, flags, startId); }
Initializing All the Things
Now that your base member variables are created, it's time to initialize everything. We'll do this by calling various helper methods in onCreate()
.
@Override public void onCreate() { super.onCreate(); initMediaPlayer(); initMediaSession(); initNoisyReceiver(); }
The first method, initMediaPlayer()
, will initialize the MediaPlayer
object that we created at the top of the class, request partial wake lock (which is why we required that permission in AndroidManifest.xml), and set the player's volume.
private void initMediaPlayer() { mMediaPlayer = new MediaPlayer(); mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mMediaPlayer.setVolume(1.0f, 1.0f); }
The next method, initMediaSession()
, is where we initialize the MediaSessionCompat
object and wire it to the media buttons and control methods that allow us to handle playback and user input. This method starts by creating a ComponentName
object that points to the Android support library's MediaButtonReceiver
class, and uses that to create a new MediaSessionCompat
. We then pass the MediaSession.Callback
object that we created earlier to it, and set the flags necessary for receiving media button inputs and control signals. Next, we create a new Intent
for handling media button inputs on pre-Lollipop devices, and set the media session token for our service.
private void initMediaSession() { ComponentName mediaButtonReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class); mMediaSessionCompat = new MediaSessionCompat(getApplicationContext(), "Tag", mediaButtonReceiver, null); mMediaSessionCompat.setCallback(mMediaSessionCallback); mMediaSessionCompat.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS ); Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setClass(this, MediaButtonReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0); mMediaSessionCompat.setMediaButtonReceiver(pendingIntent); setSessionToken(mMediaSessionCompat.getSessionToken()); }
Finally, we'll register the BroadcastReceiver
that we created at the top of the class so that we can listen for headphone change events.
private void initNoisyReceiver() { //Handles headphones coming unplugged. cannot be done through a manifest receiver IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver(mNoisyReceiver, filter); }
Handling Audio Focus
Now that you've finished initializing BroadcastReceiver
, MediaSessionCompat
and MediaPlayer
objects, it's time to look into handling audio focus.
While we may think our own audio apps are the most important at the moment, other apps on the device will be competing to make their own sounds, such as an email notification or mobile game. In order to work with these various situations, the Android system uses audio focus to determine how audio should be handled.
The first case we will want to handle is starting playback and attempting to receive the device's focus. In your MediaSessionCompat.Callback
object, go into the onPlay()
method and add the following condition check.
@Override public void onPlay() { super.onPlay(); if( !successfullyRetrievedAudioFocus() ) { return; } }
The above code will call a helper method that attempts to retrieve focus, and if it cannot, it will simply return. In a real app, you would want to handle failed audio playback more gracefully. successfullyRetrievedAudioFocus()
will get a reference to the system AudioManager
, and attempt to request audio focus for streaming music. It will then return a boolean
representing whether or not the request succeeded.
private boolean successfullyRetrievedAudioFocus() { AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); return result == AudioManager.AUDIOFOCUS_GAIN; }
You'll notice that we are also passing this
into the requestAudioFocus()
method, which associates the OnAudioFocusChangeListener
with our service. There are a few different states that you'll want to listen for in order to be a "good citizen" in the device's app ecosystem.
-
AudioManager.AUDIOFOCUS_LOSS
: This occurs when another app has requested audio focus. When this happens, you should stop audio playback in your app. -
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
: This state is entered when another app wants to play audio, but it only anticipates needing focus for a short time. You can use this state to pause your audio playback. -
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
: When audio focus is requested, but throws a 'can duck' state, it means that you can continue your playback, but should bring the volume down a bit. This can occur when a notification sound is played by the device.
-
AudioManager.AUDIOFOCUS_GAIN
: The final state we will discuss isAUDIOFOCUS_GAIN
. This is the state when a duckable audio playback has completed, and your app can resume at its previous levels.
A simplified onAudioFocusChange()
callback may look like this:
@Override public void onAudioFocusChange(int focusChange) { switch( focusChange ) { case AudioManager.AUDIOFOCUS_LOSS: { if( mMediaPlayer.isPlaying() ) { mMediaPlayer.stop(); } break; } case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: { mMediaPlayer.pause(); break; } case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: { if( mMediaPlayer != null ) { mMediaPlayer.setVolume(0.3f, 0.3f); } break; } case AudioManager.AUDIOFOCUS_GAIN: { if( mMediaPlayer != null ) { if( !mMediaPlayer.isPlaying() ) { mMediaPlayer.start(); } mMediaPlayer.setVolume(1.0f, 1.0f); } break; } } }
Understanding the MediaSessionCompat.Callback
Now that you have a general structure together for your Service
, it's time to dive into the MediaSessionCompat.Callback
. In the last section you added a little bit to onPlay()
to check if audio focus was granted. Below the conditional statement, you will want to set the MediaSessionCompat
object to active, give it a state of STATE_PLAYING
, and assign the proper actions necessary to create pause buttons on pre-Lollipop lock screen controls, phone and Android Wear notifications.
@Override public void onPlay() { super.onPlay(); if( !successfullyRetrievedAudioFocus() ) { return; } mMediaSessionCompat.setActive(true); setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING); ... }
The setMediaPlaybackState()
method above is a helper method that creates a PlaybackStateCompat.Builder
object and gives it the proper actions and state, and then builds and associates a PlaybackStateCompat
with your MediaSessionCompat
object.
private void setMediaPlaybackState(int state) { PlaybackStateCompat.Builder playbackstateBuilder = new PlaybackStateCompat.Builder(); if( state == PlaybackStateCompat.STATE_PLAYING ) { playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE); } else { playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY); } playbackstateBuilder.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0); mMediaSessionCompat.setPlaybackState(playbackstateBuilder.build()); }
It's important to note that you will need both the ACTION_PLAY_PAUSE
and either ACTION_PAUSE
or ACTION_PLAY
flags in your actions in order to get proper controls on Android Wear.
Back in onPlay()
, you will want to show a playing notification that is associated with your MediaSessionCompat
object by using the MediaStyleHelper
class that we defined earlier, and then show that notification.
private void showPlayingNotification() { NotificationCompat.Builder builder = MediaStyleHelper.from(BackgroundAudioService.this, mMediaSessionCompat); if( builder == null ) { return; } builder.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_pause, "Pause", MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE))); builder.setStyle(new NotificationCompat.MediaStyle().setShowActionsInCompactView(0).setMediaSession(mMediaSessionCompat.getSessionToken())); builder.setSmallIcon(R.mipmap.ic_launcher); NotificationManagerCompat.from(BackgroundAudioService.this).notify(1, builder.build()); }
Finally, you will start the MediaPlayer
at the end of onPlay()
.
@Override public void onPlay() { super.onPlay(); ... showPlayingNotification(); mMediaPlayer.start(); }
When the callback receives a pause command, onPause()
will be called. Here you will pause the MediaPlayer
, set the state to STATE_PAUSED
, and show a paused notification.
@Override public void onPause() { super.onPause(); if( mMediaPlayer.isPlaying() ) { mMediaPlayer.pause(); setMediaPlaybackState(PlaybackStateCompat.STATE_PAUSED); showPausedNotification(); } }
Our showPausedNotification()
helper method will look similar to the showPlayNotification()
method.
private void showPausedNotification() { NotificationCompat.Builder builder = MediaStyleHelper.from(this, mMediaSessionCompat); if( builder == null ) { return; } builder.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_play, "Play", MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE))); builder.setStyle(new NotificationCompat.MediaStyle().setShowActionsInCompactView(0).setMediaSession(mMediaSessionCompat.getSessionToken())); builder.setSmallIcon(R.mipmap.ic_launcher); NotificationManagerCompat.from(this).notify(1, builder.build()); }
The next method in the callback that we'll discuss, onPlayFromMediaId()
, takes a String
and a Bundle
as parameters. This is the callback method that you can use for changing audio tracks/content within your app.
For this tutorial, we will simply accept a raw resource ID and attempt to play that, and then reinitialize the session's metadata. As you are allowed to pass a Bundle
into this method, you can use it to customize other aspects of your media playback, such as setting up a custom background sound for a track.
@Override public void onPlayFromMediaId(String mediaId, Bundle extras) { super.onPlayFromMediaId(mediaId, extras); try { AssetFileDescriptor afd = getResources().openRawResourceFd(Integer.valueOf(mediaId)); if( afd == null ) { return; } try { mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); } catch( IllegalStateException e ) { mMediaPlayer.release(); initMediaPlayer(); mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); } afd.close(); initMediaSessionMetadata(); } catch (IOException e) { return; } try { mMediaPlayer.prepare(); } catch (IOException e) {} //Work with extras here if you want }
Now that we've discussed the two main methods in this callback that you will use in your apps, it's important to know that there are other optional methods that you can use to customize your service. Some methods include onSeekTo()
, which allows you to change the playback position of your content, and onCommand()
, which will accept a String
denoting the type of command, a Bundle
for extra information about the command, and a ResultReceiver
callback, which will allow you to send custom commands to your Service
.
@Override public void onCommand(String command, Bundle extras, ResultReceiver cb) { super.onCommand(command, extras, cb); if( COMMAND_EXAMPLE.equalsIgnoreCase(command) ) { //Custom command here } } @Override public void onSeekTo(long pos) { super.onSeekTo(pos); }
Tearing Down
When our audio file has completed, we will want to decide what our next action will be. While you may want to play the next track in your app, we'll keep things simple and release the MediaPlayer
.
@Override public void onCompletion(MediaPlayer mediaPlayer) { if( mMediaPlayer != null ) { mMediaPlayer.release(); } }
Finally, we'll want to do a few things in the onDestroy()
method of our Service
. First, get a reference to the system service's AudioManager
, and call abandonAudioFocus()
with our AudioFocusChangeListener
as a parameter, which will notify other apps on the device that you are giving up audio focus. Next, unregister the BroadcastReceiver
that was set up to listen for headphone changes, and release the MediaSessionCompat
object. Finally, you will want to cancel the playback control notification.
@Override public void onDestroy() { super.onDestroy(); AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); audioManager.abandonAudioFocus(this); unregisterReceiver(mNoisyReceiver); mMediaSessionCompat.release(); NotificationManagerCompat.from(this).cancel(1); }
At this point, you should have a working basic background audio Service
using MediaSessionCompat
for playback control across devices. While there has already been a lot involved in just creating the service, you should be able to control playback from your app, a notification, lock screen controls on pre-Lollipop devices (Lollipop and above will use the notification on the lock screen), and from peripheral devices, such as Android Wear, once the Service
has been started.
Starting and Controlling Content From an Activity
While most controls will be automatic, you will still have a bit of work to start and control a media session from your in-app controls. At the very least, you will want a MediaBrowserCompat.ConnectionCallback
, MediaControllerCompat.Callback
, MediaBrowserCompat
, and MediaControllerCompat
objects created in your app.
MediaControllerCompat.Callback
will have a method called onPlaybackStateChanged()
that receives changes in playback state, and can be used to keep your UI in sync.
private MediaControllerCompat.Callback mMediaControllerCompatCallback = new MediaControllerCompat.Callback() { @Override public void onPlaybackStateChanged(PlaybackStateCompat state) { super.onPlaybackStateChanged(state); if( state == null ) { return; } switch( state.getState() ) { case PlaybackStateCompat.STATE_PLAYING: { mCurrentState = STATE_PLAYING; break; } case PlaybackStateCompat.STATE_PAUSED: { mCurrentState = STATE_PAUSED; break; } } } };
MediaBrowserCompat.ConnectionCallback
has an onConnected()
method that will be called when a new MediaBrowserCompat
object is created and connected. You can use this to initialize your MediaControllerCompat
object, link it to your MediaControllerCompat.Callback
, and associate it with MediaSessionCompat
from your Service
. Once that is completed, you can start audio playback from this method.
private MediaBrowserCompat.ConnectionCallback mMediaBrowserCompatConnectionCallback = new MediaBrowserCompat.ConnectionCallback() { @Override public void onConnected() { super.onConnected(); try { mMediaControllerCompat = new MediaControllerCompat(MainActivity.this, mMediaBrowserCompat.getSessionToken()); mMediaControllerCompat.registerCallback(mMediaControllerCompatCallback); setSupportMediaController(mMediaControllerCompat); getSupportMediaController().getTransportControls().playFromMediaId(String.valueOf(R.raw.warner_tautz_off_broadway), null); } catch( RemoteException e ) { } } };
You'll notice that the above code snippet uses getSupportMediaController().getTransportControls()
to communicate with the media session. Using the same technique, you can call onPlay()
and onPause()
in your audio service's MediaSessionCompat.Callback
object.
if( mCurrentState == STATE_PAUSED ) { getSupportMediaController().getTransportControls().play(); mCurrentState = STATE_PLAYING; } else { if( getSupportMediaController().getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) { getSupportMediaController().getTransportControls().pause(); } mCurrentState = STATE_PAUSED; }
When you're done with your audio playback, you can pause the audio service and disconnect your MediaBrowserCompat
object, which we'll do in this tutorial when this Activity
is destroyed.
@Override protected void onDestroy() { super.onDestroy(); if( getSupportMediaController().getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) { getSupportMediaController().getTransportControls().pause(); } mMediaBrowserCompat.disconnect(); }
Wrapping Up
Whew! As you can see, there are a lot of moving pieces involved with creating and using a background audio service correctly.
In this tutorial, you have created a Service that plays a simple audio file, listens for changes in audio focus, and links to MediaSessionCompat
to provide universal playback control on Android devices, including handsets and Android Wear. If you run into roadblocks while working through this tutorial, I strongly recommend checking out the associated Android project code on Envato Tuts+'s GitHub.
And check out some of our other Android courses and tutorials here on Envato Tuts+!
- Android SDKWhat's New in Android N and Wear 2.0
- Android SDKGoogle Play Services: Google Cast v3 and Media
- Android SDKTake Pictures With Your Android App
Comments