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

You can either download all the sample code to your computer...

Download Zip

...or clone the GitHub repository from the command line.

$ git clone https://github.com/googlecodelabs/android-wear-auto-uamp.git

First, we will create a very basic music player that plays predefined music in an activity. This is not the recommended way of playing music on Android. Let's try it to understand why.

  1. Open Android Studio.
  2. Select the android-wear-auto-uamp/base directory from the code folder (File > Import Project... > android-wear-auto-uamp/base).
  3. Enable USB debugging and plug in your Android device.
  4. Click the Android Studio Run button (or press shift+F10).
  5. 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.

Frequently Asked Questions

In general, a user expects music to continue playing in the background while they do other things on their device. To support background playing, we need to refactor our music player to play from a service, instead of playing from an Activity as it does now.

In Android Lollipop, we introduced a special abstract service, the MediaBrowserService, 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 MediaBrowserService, you gain compatibility with Android Auto and Android Wear (almost for) free . Let's see how to do it!

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. Android Studio creates a new class called MusicService.java. Replace the contents of this class with the code below:

MusicService.java

package com.example.android.musicplayercodelab;

import android.media.MediaMetadata;
import android.media.browse.MediaBrowser.MediaItem;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.service.media.MediaBrowserService;

import java.util.List;

public class MusicService extends MediaBrowserService {

    private MediaSession mSession;
    private PlaybackManager mPlayback;

    final MediaSession.Callback mCallback = new MediaSession.Callback() {
        @Override
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
            mSession.setActive(true);
            MediaMetadata 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 MediaSession(this, "MusicService");
        mSession.setCallback(mCallback);
        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
                MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
        setSessionToken(mSession.getSessionToken());

        // ------------ CHANGE 7 - UNCOMMENT THE FOLLOWING LINE TO USE A NOTIFICATION
        // final MediaNotificationManager mediaNotificationManager = new MediaNotificationManager(this);
        // ------------ CHANGE 7 - END OF CHANGE

        mPlayback = new PlaybackManager(this, new PlaybackManager.Callback() {
            @Override
            public void onPlaybackStatusChanged(PlaybackState state) {
                mSession.setPlaybackState(state);
                // ------------ CHANGE 8 - UNCOMMENT THE FOLLOWING LINE TO USE A NOTIFICATION
                // mediaNotificationManager.update(mPlayback.getCurrentMedia(), state, getSessionToken());
                // ------------ CHANGE 8 - END OF CHANGE
            }
        });
    }

    @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<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

We now have a service that provides content and handles playback, but the activity is still playing the media content. We need to change the Activity so that the service provides the media instead.

  1. Open MusicPlayerActivity.java.
  2. There are 6 places in this class with special comments, similar to:"---------- CHANGE 1". Locate these comments and follow the instructions in the source code: remove the lines that appear after the initial comment, and uncomment the second block of code.


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

// ------------ CHANGE 6 - REMOVE FOLLOWING LINES FOR PLAYBACK ON A SERVICE
mPlaybackManager.pause();

/* ------------ CHANGE 6 - UNCOMMENT FOLLOWING LINES FOR PLAYBACK ON A SERVICE
getMediaController().getTransportControls().pause();
// ------------ CHANGE 6 - END OF PLAYBACK ON A SERVICE SNIPPET */

after following the instructions, the file should contain only the following line:

getMediaController().getTransportControls().pause();

Repeat this process for all 6 changes.

  1. If necessary, fix the missing import statements, adding references to classes such asandroid.media.session.MediaController and android.content.ComponentName.

Now your activity is using the MediaBrowserService 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

Even after creating a service to handle media playback, this service alone doesn't allow media to continue playing for an extended period of time. The reason is that the system can stop a service that is not connected to any client apps or activities. We need to tell Android that this service needs to continue running while the music is playing.

We provide this instruction to Android 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 that is associated with a MediaSession.

  1. Create a new Java file called MediaNotificationManager in the same directory/package as the other classes:
  1. Paste the following code:


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.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;

/**
 * Keeps track of a notification and updates it automatically for a given
 * MediaSession. This is required so that the music service
 * doesn'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 Notification.Action mPlayAction;
    private final Notification.Action mPauseAction;
    private final Notification.Action mNextAction;
    private final Notification.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 Notification.Action(R.drawable.ic_play_arrow_white_24dp,
                mService.getString(R.string.label_play), playIntent);
        mPauseAction = new Notification.Action(R.drawable.ic_pause_white_24dp,
                mService.getString(R.string.label_pause), pauseIntent);
        mNextAction = new Notification.Action(R.drawable.ic_skip_next_white_24dp,
                mService.getString(R.string.label_next), nextIntent);
        mPrevAction = new Notification.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(MediaMetadata metadata, PlaybackState state, MediaSession.Token token) {
        if (state == null || state.getState() == PlaybackState.STATE_STOPPED ||
                state.getState() == PlaybackState.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() == PlaybackState.STATE_PLAYING;
        Notification.Builder notificationBuilder = new Notification.Builder(mService);
        MediaDescription description = metadata.getDescription();

        notificationBuilder
                .setStyle(new Notification.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() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
            notificationBuilder.addAction(mPrevAction);
        }

        notificationBuilder.addAction(isPlaying ? mPauseAction : mPlayAction);

        // If skip to prev action is enabled
        if ((state.getActions() & PlaybackState.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. In the MusicService class, there are two places with special comments (CHANGE 7 and CHANGE 8). Locate these two comments and follow the instructions in the source code: remove the lines that appear after the initial comment and uncomment the second block of code.
    For example, if the original code file displays these comments

MusicService.java

// ------------ CHANGE 7 - UNCOMMENT THE FOLLOWING LINE TO USE A NOTIFICATION
// final MediaNotificationManager mediaNotificationManager = new MediaNotificationManager(this);
// ------------ CHANGE 7 - END OF CHANGE

the edited code includes the following line after you make the change:

MusicService.java

final MediaNotificationManager mediaNotificationManager = new MediaNotificationManager(this);

Repeat this process for both changes.

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.

Also, because you've used a MediaStyle notification, the album art of the current song should appear as the background of the lock screen:

Takeaways

Look at a paired Android Wear watch while a song is playing. You should 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 MediaSession linked by a MediaStyle notification. The latest version of Android Wear also connects to the MediaBrowserService for browsing content.

Takeaways

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

  1. Create a new file "res/xml/automotive_app_desc.xml":

Right click in the "res" folder and select File -> New -> Android Resource File


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. Edit AndroidManifest.xml and include the following meta-data 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! Deploy the modified app to your phone. If you are using Android Auto Desktop Head Unit, your app is ready to test. If you are testing on a physical head unit, you will need to publish to an alpha channel on Google Play Store, so that you can test it. Android Auto doesn't accept sideloaded apps.

Click twice on the the Music (headphones) icon:

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 with minimal effort. Using the Android Lollipop media APIs enables your app to seamlessly play music where your user wants, with minimal 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:

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