In this codelab, you'll learn how to adapt a music player app to work seamlessly on Android Auto and Android Wear using the latest Android media APIs.

What you'll learn

What you'll need

How would you rate your experience with building Android apps?

Novice Intermediate Proficient

If you have Git installed, you can simply run the command below. (You can check if Git is installed by typing git --version in the terminal / command line and verify it executes correctly.)

Download Zip

If you do not have Git, you can download the project as a zip file:

$ git clone https://github.com/googlecodelabs/musicplayer-devices.git

We'll begin with a basic music player that plays predefined music in an activity. This is not the recommended way of playing music on Android, but it will provide a framework to build something better.

  1. Open Android Studio.
  2. Select the android-wear-auto-uamp directory from the code folder (File > Import Project... > android-wear-auto-uamp).
  3. Enable USB debugging and plug in your Android device.
  4. Select the "base" module
  5. Click the Android Studio Run button (or press shift+F10).
  6. After a few seconds, you should see the music player app similar to the screenshot below:

Now, play with it. In particular, try these things:

  1. While playing a song, go back to the Android home
  2. While playing a song, lock the phone (press the power button)

You'll notice that the music stops playing when you leave the app.

A user generally expects their music to continue playing in the background or with the screen off. The app needs to be refactored in order to play music from a service to allow this.

The compatibility library includes a specialized abstract service, MediaBrowserServiceCompat, which simplifies how you expose media content and playback controls to consumers. These consumers can be different parts of the same application, or external apps. By exporting a service that extends MediaBrowserServiceCompat, you gain compatibility with Android Auto and Android Wear with very little additional work.

Create a MediaBrowserService

  1. In Android Studio, right click on your project's package folder and select New > Service > Service.

  1. Fill the dialog with the following information:
    Name: MusicService
    Exported: true (keep the default option)
    Enabled: true (keep the default option)

  1. Replace the contents MusicService with the code below:

MusicService.java

package com.example.android.musicplayercodelab;

import android.os.Bundle;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaBrowserServiceCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import java.util.List;

public class MusicService extends MediaBrowserServiceCompat {

   private MediaSessionCompat mSession;
   private PlaybackManager mPlayback;

   final MediaSessionCompat.Callback mCallback =
           new MediaSessionCompat.Callback() {
               @Override
               public void onPlayFromMediaId(String mediaId, Bundle extras) {
                   mSession.setActive(true);
                   MediaMetadataCompat metadata =
                           MusicLibrary.getMetadata(MusicService.this, mediaId);
                   mSession.setMetadata(metadata);
                   mPlayback.play(metadata);
               }

               @Override
               public void onPlay() {
                   if (mPlayback.getCurrentMediaId() != null) {
                       onPlayFromMediaId(mPlayback.getCurrentMediaId(), null);
                   }
               }

               @Override
               public void onPause() {
                   mPlayback.pause();
               }

               @Override
               public void onStop() {
                   stopSelf();
               }

               @Override
               public void onSkipToNext() {
                   onPlayFromMediaId(
                           MusicLibrary.getNextSong(mPlayback.getCurrentMediaId()), null);
               }

               @Override
               public void onSkipToPrevious() {
                   onPlayFromMediaId(
                           MusicLibrary.getPreviousSong(mPlayback.getCurrentMediaId()), null);
               }
           };

   @Override
   public void onCreate() {
       super.onCreate();

       // Start a new MediaSession
       mSession = new MediaSessionCompat(this, "MusicService");
       mSession.setCallback(mCallback);
       mSession.setFlags(
               MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
                       | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
       setSessionToken(mSession.getSessionToken());

       // TODO: Uncomment the following line to show a notification
       // final MediaNotificationManager mediaNotificationManager = new MediaNotificationManager(this);

       mPlayback =
               new PlaybackManager(
                       this,
                       new PlaybackManager.Callback() {
                           @Override
                           public void onPlaybackStatusChanged(PlaybackStateCompat state) {
                               mSession.setPlaybackState(state);

                               // TODO: Uncomment the following line to show a notification
                               // mediaNotificationManager.update(mPlayback.getCurrentMedia(), state, getSessionToken());
                           }
                       });
   }

   @Override
   public void onDestroy() {
       mPlayback.stop();
       mSession.release();
   }

   @Override
   public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
       return new BrowserRoot(MusicLibrary.getRoot(), null);
   }

   @Override
   public void onLoadChildren(
           final String parentMediaId, final Result<List<MediaBrowserCompat.MediaItem>> result) {
       result.sendResult(MusicLibrary.getMediaItems());
   }
}
  1. Open AndroidManifest.xml, locate the <service> element associated with the MusicService, then add an <intent-filter> with action "android.media.browse.MediaBrowserService"

AndroidManifest.xml

<service
     android:name=".MusicService"
     android:enabled="true"
     android:exported="true" >
     <intent-filter>
          <action android:name="android.media.browse.MediaBrowserService" />
     </intent-filter>
</service>

Update the Activity to connect to the service

There is now a service that provides media content and handles playback, but it isn't being used yet. Change the Activity so that it utilizes the service by following these steps:

  1. Open MusicPlayerActivity.
  2. There are 6 places in this class with TODO comments, similar to: // TODO: [1] Remove the following line for playback in a Service. Locate these comments and follow the instructions in the source code.


For example, if the original code file displays these comments:

// TODO: [6] Remove the following line for playback in a Service
mPlaybackManager.pause();

// TODO: [5] Uncomment the following block for playback in a Service
/*
MediaControllerCompat.getMediaController(MusicPlayerActivity.this)
       .getTransportControls()
       .pause();
*/

After following the instructions, the code should read:

MediaControllerCompat.getMediaController(MusicPlayerActivity.this)
       .getTransportControls()
       .pause();

Repeat this process for all 6 changes.

  1. Some additional imports will need to be added, such as android.support.v4.media.session.MediaControllerCompat and android.content.ComponentName. Using "Import class" will automatically perform these updates.

Now your activity is using the MediaBrowserServiceCompat to play music. Run and test the app again.

Surprisingly, the music still eventually stops when you lock the phone screen or start doing something else on the device. The next section explains why.

Takeaways

Using a service does not automatically prevent playback from stopping once the app goes into the background. The service must declare to the OS, and the user, that it is still doing work. This is done is by creating a notification that is linked to the service. In this case, we use a MediaStyle notification, which is a special kind of notification associated with a MediaSessionCompat.

  1. In Android Studio, right click on your project's package folder and select New > Java class.
  2. Fill the dialog with the following information:
    Name: MediaNotificationManager
    Kind: Class
    Superclass: leave empty
    Interface(s): leave empty
    Package: com.example.android.musicplayercodelab
    Visibility: Public
    Modifiers: None
    Show Select Overrides Dialog: leave unchecked

  1. Replace the contents MediaNotificationManager with the code below:


MediaNotificationManager.java

package com.example.android.musicplayercodelab;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;

/**
* Keeps track of a notification and updates it automatically for a given MediaSession. This is
* required so that the music service don't get killed during playback.
*/
public class MediaNotificationManager extends BroadcastReceiver {
   private static final int NOTIFICATION_ID = 412;
   private static final int REQUEST_CODE = 100;

   private static final String ACTION_PAUSE = "com.example.android.musicplayercodelab.pause";
   private static final String ACTION_PLAY = "com.example.android.musicplayercodelab.play";
   private static final String ACTION_NEXT = "com.example.android.musicplayercodelab.next";
   private static final String ACTION_PREV = "com.example.android.musicplayercodelab.prev";

   private final MusicService mService;

   private final NotificationManager mNotificationManager;

   private final NotificationCompat.Action mPlayAction;
   private final NotificationCompat.Action mPauseAction;
   private final NotificationCompat.Action mNextAction;
   private final NotificationCompat.Action mPrevAction;

   private boolean mStarted;

   public MediaNotificationManager(MusicService service) {
       mService = service;

       String pkg = mService.getPackageName();
       PendingIntent playIntent =
               PendingIntent.getBroadcast(
                       mService,
                       REQUEST_CODE,
                       new Intent(ACTION_PLAY).setPackage(pkg),
                       PendingIntent.FLAG_CANCEL_CURRENT);
       PendingIntent pauseIntent =
               PendingIntent.getBroadcast(
                       mService,
                       REQUEST_CODE,
                       new Intent(ACTION_PAUSE).setPackage(pkg),
                       PendingIntent.FLAG_CANCEL_CURRENT);
       PendingIntent nextIntent =
               PendingIntent.getBroadcast(
                       mService,
                       REQUEST_CODE,
                       new Intent(ACTION_NEXT).setPackage(pkg),
                       PendingIntent.FLAG_CANCEL_CURRENT);
       PendingIntent prevIntent =
               PendingIntent.getBroadcast(
                       mService,
                       REQUEST_CODE,
                       new Intent(ACTION_PREV).setPackage(pkg),
                       PendingIntent.FLAG_CANCEL_CURRENT);

       mPlayAction =
               new NotificationCompat.Action(
                       R.drawable.ic_play_arrow_white_24dp,
                       mService.getString(R.string.label_play),
                       playIntent);
       mPauseAction =
               new NotificationCompat.Action(
                       R.drawable.ic_pause_white_24dp,
                       mService.getString(R.string.label_pause),
                       pauseIntent);
       mNextAction =
               new NotificationCompat.Action(
                       R.drawable.ic_skip_next_white_24dp,
                       mService.getString(R.string.label_next),
                       nextIntent);
       mPrevAction =
               new NotificationCompat.Action(
                       R.drawable.ic_skip_previous_white_24dp,
                       mService.getString(R.string.label_previous),
                       prevIntent);

       IntentFilter filter = new IntentFilter();
       filter.addAction(ACTION_NEXT);
       filter.addAction(ACTION_PAUSE);
       filter.addAction(ACTION_PLAY);
       filter.addAction(ACTION_PREV);

       mService.registerReceiver(this, filter);

       mNotificationManager =
               (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);

       // Cancel all notifications to handle the case where the Service was killed and
       // restarted by the system.
       mNotificationManager.cancelAll();
   }

   @Override
   public void onReceive(Context context, Intent intent) {
       final String action = intent.getAction();
       switch (action) {
           case ACTION_PAUSE:
               mService.mCallback.onPause();
               break;
           case ACTION_PLAY:
               mService.mCallback.onPlay();
               break;
           case ACTION_NEXT:
               mService.mCallback.onSkipToNext();
               break;
           case ACTION_PREV:
               mService.mCallback.onSkipToPrevious();
               break;
       }
   }

   public void update(
           MediaMetadataCompat metadata,
           PlaybackStateCompat state,
           MediaSessionCompat.Token token) {
       if (state == null
               || state.getState() == PlaybackStateCompat.STATE_STOPPED
               || state.getState() == PlaybackStateCompat.STATE_NONE) {
           mService.stopForeground(true);
           try {
               mService.unregisterReceiver(this);
           } catch (IllegalArgumentException ex) {
               // ignore receiver not registered
           }
           mService.stopSelf();
           return;
       }
       if (metadata == null) {
           return;
       }
       boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_PLAYING;
       NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(mService);
       MediaDescriptionCompat description = metadata.getDescription();

       notificationBuilder
               .setStyle(
                       new NotificationCompat.MediaStyle()
                               .setMediaSession(token)
                               .setShowActionsInCompactView(0, 1, 2))
               .setColor(
                       mService.getApplication().getResources().getColor(R.color.notification_bg))
               .setSmallIcon(R.drawable.ic_notification)
               .setVisibility(Notification.VISIBILITY_PUBLIC)
               .setContentIntent(createContentIntent())
               .setContentTitle(description.getTitle())
               .setContentText(description.getSubtitle())
               .setLargeIcon(MusicLibrary.getAlbumBitmap(mService, description.getMediaId()))
               .setOngoing(isPlaying)
               .setWhen(isPlaying ? System.currentTimeMillis() - state.getPosition() : 0)
               .setShowWhen(isPlaying)
               .setUsesChronometer(isPlaying);

       // If skip to next action is enabled
       if ((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) {
           notificationBuilder.addAction(mPrevAction);
       }

       notificationBuilder.addAction(isPlaying ? mPauseAction : mPlayAction);

       // If skip to prev action is enabled
       if ((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) {
           notificationBuilder.addAction(mNextAction);
       }

       Notification notification = notificationBuilder.build();

       if (isPlaying && !mStarted) {
           mService.startService(new Intent(mService.getApplicationContext(), MusicService.class));
           mService.startForeground(NOTIFICATION_ID, notification);
           mStarted = true;
       } else {
           if (!isPlaying) {
               mService.stopForeground(false);
               mStarted = false;
           }
           mNotificationManager.notify(NOTIFICATION_ID, notification);
       }
   }

   private PendingIntent createContentIntent() {
       Intent openUI = new Intent(mService, MusicPlayerActivity.class);
       openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
       return PendingIntent.getActivity(
               mService, REQUEST_CODE, openUI, PendingIntent.FLAG_CANCEL_CURRENT);
   }
}
  1. Now we need to make changes in a separate class. In the MusicService class, there are two TODOs in the code: // TODO: Uncomment the following line to show a notification. Locate them and follow the instructions to utilize MediaNotificationManager:

MusicService.java

// TODO: Uncomment the following line to show a notification
// final MediaNotificationManager mediaNotificationManager = new MediaNotificationManager(this);

MusicService.java

final MediaNotificationManager mediaNotificationManager = new MediaNotificationManager(this);

MusicService.java

// TODO: Uncomment the following line to show a notification
// mediaNotificationManager.update(mPlayback.getCurrentMedia(), state, getSessionToken());

MusicService.java

mediaNotificationManager.update(mPlayback.getCurrentMedia(), state, getSessionToken());

Run!

Repeat the same tests you did before: start playing, do something else, open another application, and lock the screen. Notice that the music continues to play in the background.

The album art of the current song appears as the background of the lock screen, along with media controls. This is because you've used a MediaStyle notification.

Takeaways

Look at a paired Android Wear watch while a song is playing and you'll see a special media notification that allows you to control playback and browse media content.

There is not a single line of Wear specific code in this project. Android Wear automatically connects to the MediaSessionCompat linked by a MediaStyle notification. The latest version of Android Wear also connects to the MediaBrowserServiceCompat for browsing content.

Takeaways

Adapting the app to run on Android Auto is very simple. Android Auto, like Android Wear, relies on the standard MediaBrowserServiceCompat and MediaSessionCompat APIs. The only additional requirement is to declare in your manifest that your app is compatible with Android Auto:

  1. In Android Studio, right click on your project's "res" folder and select New > Android Resource File.

  1. Then fill the dialog with the following info:

File name: automotive_app_desc
Resource type: XML
Root element: automotiveApp

Use the default settings for all other options, then select OK.

  1. Replace the content of the file you just created with:

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="media"/>
</automotiveApp>
  1. Add the following meta-data tag to AndroidManifest.xml, inside the application tag:

AndroidManifest.xml

<meta-data
   android:name="com.google.android.gms.car.application"
   android:resource="@xml/automotive_app_desc" />

Now your music player is ready to work on Android Auto! In the next step you'll see how to test it.

Connect and start the Android Auto Head Unit following the instructions here: https://developer.android.com/training/auto/testing/index.html.

If you're using Android Auto Desktop Head Unit, simply deploy it to your phone and it's ready to test. If you want to test on a physical head unit, you need to publish the APK to an alpha channel on Google Play Store. Android Auto, for safety reasons, doesn't accept sideloaded apps.

Access to the app is provided via the headphones icon. Tapping it twice allows choosing an app:

Select your app ("Music Player Codelab") from the list and have fun with it!

Takeaways

Your music player now plays media on a service and allows for media browsing and playback controls on Android Wear and Android Auto. Using the media compatibility library APIs enables your app to seamlessly play music where your user wants, with minimal additional development and maintenance effort from you.

What we've covered

Next Steps

This codelab ends here, but if you want to add more features, you can try the following:

Take a look at our open source reference music player, Universal Android Music Player (UAMP), which shows how to handle many issues you will encounter when creating a multi-form factor music player on Android.