Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Build media apps for cars

Android Auto and Android Automotive OS help you bring your media app content to users in their car. A media app for cars must provide a media browser service so that Android Auto and Android Automotive OS (or another app with a media browser) can discover and display your content.

This guide assumes that you already have a media app that plays audio on a phone and that your media app conforms to the Android media app architecture.

This guide describes the required components of a MediaBrowserService and MediaSession that your app needs in order to work on Android Auto or Android Automotive OS. After you have completed the core media infrastructure, you can add support for Android Auto and add support for Android Automotive OS to your media app.

Before you begin

  1. Review the Android media API documentation.
  2. Review the Android Automotive OS app design guidelines and Android Auto app design guidelines.
  3. Review the key terms and concepts listed in this section.

Key terms and concepts

Media Browser Service
An Android service implemented by your media app that complies with the MediaBrowserServiceCompat API. Your app uses this service to expose its content.
Media Browser
An API used by media apps to discover media browser services and display their content. Android Auto and Android Automotive OS use a media browser to find your app's media browser service.
Media Item

The media browser organizes its content in a tree of MediaItem objects. A media item can have either or both of these two flags:

  • Playable: This flag indicates that the item is a leaf on the content tree. The item represents a single sound stream such as a song on an album, a chapter in an audio book, or an episode of a podcast.
  • Browsable: This flag indicates the item is a node on the content tree and it has children. For example, the item represents an album and its children are the songs on the album.

A media item that is both browsable and playable acts like a playlist. You can select the item itself to play all of its children, or you can browse its children.

Vehicle-Optimized

An activity for an Android Automotive OS app that adheres to the Android Automotive OS design guidelines. The interface for these activities is not drawn by Android Automotive OS, so you must ensure that your app adheres to the design guidelines. Typically, this includes larger tap targets and font sizes, support for day and night modes, and higher contrast ratios.

Vehicle-optimized user interfaces are only allowed to be displayed when Car User Experience Restrictions (CUXRs) are not in effect because these interfaces can require extended attention or interaction from the user. CUXRs are not in effect when the car is stopped or parked but are always in effect when the car is in motion.

You don't need to design activities for Android Auto because Android Auto draws its own vehicle-optimized interface using the information from your media browser service.

Configure your app's manifest files

Before you can create your media browser service, you need to configure your app's manifest files.

Declare your media browser service

Both Android Auto and Android Automotive OS connect to your app through your media browser service in order to browse media items. You declare your Media Browse service in your manifest to allow Android Auto and Android Automotive OS to discover the service and connect to your app.

The following code snippet shows how to declare your media browser service in your manifest. You should include this code in the manifest file for your Android Automotive OS module and in the manifest file for your phone app.

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

Specify an app icon

You need to specify an app icon that Android Auto and Android Automotive OS can use to represent your app in the system UI.

You can specify an icon that is used to represent your app using the following manifest declaration:

<!--The android:icon attribute is used by Android Automotive OS-->
<application
    ...
    android:icon="@mipmap/ic_launcher">
    ...
    <!--Used by Android Auto-->
    <meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
               android:resource="@drawable/ic_auto_icon" />
    ...
<application>

Create your media browser service

You create a media browser service by extending the MediaBrowserServiceCompat class. Both Android Auto and Android Automotive OS can then use your service to do the following:

  • Browse your app's content hierarchy, in order to present a menu to the user.
  • Get the token for your app's MediaSessionCompat object, in order to control audio playback.

Media browser service workflow

This section describes how Android Automotive OS and Android Auto interact with your media browser service during a typical user workflow.

  1. A user launches your app on Android Automotive OS or Android Auto.
  2. Android Automotive OS or Android Auto contacts your app's media browser service using the onCreate() method. In your implementation of the onCreate() method, you must create and register a MediaSessionCompat object and its callback object.
  3. Android Automotive OS or Android Auto calls your service's onGetRoot() method to get the root media item in your content hierarchy. The root media item is not displayed; instead, it's used to retrieve more content from your app.
  4. Android Automotive OS or Android Auto calls your service's onLoadChildren() method to get the children of the root media item. Android Automotive OS and Android Auto display these media items as the top level of content items. Top-level content items should be browsable.
  5. If the user selects a browsable media item, your service's onLoadChildren() method is called again to retrieve the children of the selected menu item.
  6. If the user selects a playable media item, Android Automotive OS or Android Auto calls the appropriate media session callback method to perform that action.
  7. If supported by your app, the user can also search your content. In this case, Android Automotive OS or Android Auto call your service's onSearch() method.

Build your content hierarchy

Android Auto and Android Automotive OS call your app's media browser service to find out what content is available. You need to implement two methods in your media browser service to support this: onGetRoot() and onLoadChildren().

Implement onGetRoot

Your service's onGetRoot() method returns information about the root node of your content hierarchy. Android Auto and Android Automotive OS use this root node to request the rest of your content using the onLoadChildren() method.

The following code snippet shows a simple implementation of the onGetRoot() method:

Kotlin

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? =
    // Verify that the specified package is allowed to access your
    // content! You'll need to write your own logic to do this.
    if (!isValid(clientPackageName, clientUid)) {
        // If the request comes from an untrusted package, return null.
        // No further calls will be made to other media browsing methods.

        null
    } else MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null)

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {

    // Verify that the specified package is allowed to access your
    // content! You'll need to write your own logic to do this.
    if (!isValid(clientPackageName, clientUid)) {
        // If the request comes from an untrusted package, return null.
        // No further calls will be made to other media browsing methods.

        return null;
    }

    return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null);
}

For a more detailed example of this method, see the onGetRoot() method in the Universal Android Music Player sample app on GitHub.

Add package validation for onGetRoot()

When a call is made to your service's onGetRoot() method, the calling package passes identifying information to your service. Your service can use this information to decide if that package can access your content. For example, you can restrict access to your app's content to a list of approved packages by comparing the clientPackageName to your whitelist and verifying the certificate used to sign the package's APK. If the package can't be verified, return null to deny access to your content.

In order to provide system apps (such as Android Auto and Android Automotive OS) with access to your content, your service should always return a non-null BrowserRoot when these system apps call the onGetRoot() method.

The following code snippet shows how your service can validate that the calling package is a system app:

fun isKnownCaller(
    callingPackage: String,
    callingUid: Int
): Boolean {
    ...
    val isCallerKnown = when {
       // If the system is making the call, allow it.
       callingUid == Process.SYSTEM_UID -> true
       // If the app was signed by the same certificate as the platform
       // itself, also allow it.
       callerSignature == platformSignature -> true
       // ... more cases
    }
    return isCallerKnown
}

This code snippet is an excerpt from the PackageValidator class in the Universal Android Music Player sample app on GitHub. See that class for a more detailed example of how to implement package validation for your service's onGetRoot() method.

Implement onLoadChildren()

After receiving your root node object, Android Auto and Android Automotive OS build a top-level menu by calling onLoadChildren() on the root node object to get its children. Client apps build submenus by calling this same method using child node objects.

Each node in your content hierarchy is represented by a MediaBrowserCompat.MediaItem object. Each of these media items is identified by a unique ID string. Client apps treat these ID strings as opaque tokens. When a client app wants to browse to a submenu, or play a media item, it passes the token. Your app is responsible for associating the token with the appropriate media item.

Note: Android Auto and Android Automotive OS have strict limits on how many media items they can display in each level of the menu. These limits minimize distractions for drivers and help them operate your app with voice commands. For more information, see Browsing content details and Android Auto app drawer.

The following code snippet shows a simple implementation of onLoadChildren() method:

Kotlin

override fun onLoadChildren(
    parentMediaId: String,
    result: Result<List<MediaBrowserCompat.MediaItem>>
) {
    // Assume for example that the music catalog is already loaded/cached.

    val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID == parentMediaId) {

        // build the MediaItem objects for the top level,
        // and put them in the mediaItems list
    } else {

        // examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list
    }
    result.sendResult(mediaItems)
}

Java

@Override
public void onLoadChildren(final String parentMediaId,
    final Result<List<MediaBrowserCompat.MediaItem>> result) {

    // Assume for example that the music catalog is already loaded/cached.

    List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) {

        // build the MediaItem objects for the top level,
        // and put them in the mediaItems list
    } else {

        // examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list
    }
    result.sendResult(mediaItems);
}

For a complete example of this method, see the onLoadChildren() method in the Universal Android Music Player sample app on GitHub.

Apply content styles

After building your content hierarchy using browsable or playable items, you can apply content styles that determine how those items display in the car.

You can use the following content styles:

List items

This content style prioritizes titles and metadata over images.

Grid items

This content style prioritizes images over titles and metadata.

Set default content styles

You can set global defaults for how your media items are displayed by including certain constants in the BrowserRoot extras bundle of your service's onGetRoot() method. Android Auto and Android Automotive OS read the extras bundle associated with each item in the browse tree and look for those constants to determine the appropriate style.

Use the following code to declare these constants in your app:

Kotlin

/** Declares that ContentStyle is supported */
val CONTENT_STYLE_SUPPORTED = "android.media.browse.CONTENT_STYLE_SUPPORTED"

/**
* Bundle extra indicating the presentation hint for playable media items.
*/
val CONTENT_STYLE_PLAYABLE_HINT = "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"

/**
* Bundle extra indicating the presentation hint for browsable media items.
*/
val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"

/**
* Specifies the corresponding items should be presented as lists.
*/
val CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1

/**
* Specifies that the corresponding items should be presented as grids.
*/
val CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2

Java

/** Declares that ContentStyle is supported */
public static final String CONTENT_STYLE_SUPPORTED =;
   "android.media.browse.CONTENT_STYLE_SUPPORTED";

/**
* Bundle extra indicating the presentation hint for playable media items.
*/
public static final String CONTENT_STYLE_PLAYABLE_HINT =
   "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT";

/**
* Bundle extra indicating the presentation hint for browsable media items.
*/
public static final String CONTENT_STYLE_BROWSABLE_HINT =
   "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT";

/**
* Specifies the corresponding items should be presented as lists.
*/
public static final int CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1;

/**
* Specifies that the corresponding items should be presented as grids.
*/
public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;

After you have declared these constants, include them in the extras bundle of your service's onGetRoot() method to set the default content style. The following code snippet shows how to set the default content style for browsable items to grids and playable items to lists:

Kotlin

@Nullable
override fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putBoolean(CONTENT_STYLE_SUPPORTED, true)
    extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE)
    extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
    return BrowserRoot(ROOT_ID, extras)
}

Java

@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
   @Nullable Bundle rootHints) {
   Bundle extras = new Bundle();
   extras.putBoolean(CONTENT_STYLE_SUPPORTED, true);
   extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
   extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
   return new BrowserRoot(ROOT_ID, extras);
}

Set per-item content styles

The Content Style API lets you override the default content style for any browsable media item's children. To override the default, create an extras bundle in the MediaDescription of a media item.

The following code snippet shows how to create a browsable MediaItem that overrides the default content style:

Kotlin

private fun createBrowsableMediaItem(
    mediaId: String,
    folderName: String,
    iconUri: Uri
): MediaBrowser.MediaItem {
    val mediaDescriptionBuilder = MediaDescription.Builder()
    mediaDescriptionBuilder.setMediaId(mediaId)
    mediaDescriptionBuilder.setTitle(folderName)
    mediaDescriptionBuilder.setIconUri(iconUri)
    val extras = Bundle()
    extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
    extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE)
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)
}

Java

private MediaBrowser.MediaItem createBrowsableMediaItem(String mediaId,
   String folderName, Uri iconUri) {
   MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
   mediaDescriptionBuilder.setMediaId(mediaId);
   mediaDescriptionBuilder.setTitle(folderName);
   mediaDescriptionBuilder.setIconUri(iconUri);
   Bundle extras = new Bundle();
   extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE);
   extras.putInt(CONTENT_STYLE_PLAYABLE_HINT, CONTENT_STYLE_GRID_ITEM_HINT_VALUE);
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}

Group items using title hints

To group related media items together, you use a per-item hint. Every media item in a group needs to declare an extras bundle in their MediaDescription that uses an identical string. This string is used as the title of the group and can be localized.

The following code snippet shows how to create a MediaItem with a subgroup heading of "Songs":

Kotlin

val EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT = "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"

private fun createMediaItem(
    mediaId: String,
    folderName: String,
    iconUri: Uri
): MediaBrowser.MediaItem {
    val mediaDescriptionBuilder = MediaDescription.Builder()
    mediaDescriptionBuilder.setMediaId(mediaId)
    mediaDescriptionBuilder.setTitle(folderName)
    mediaDescriptionBuilder.setIconUri(iconUri)
    val extras = Bundle()
    extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), /* playable or browsable flag*/)
}

Java

public static final String EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT =
  "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT";

private MediaBrowser.MediaItem createMediaItem(String mediaId, String folderName, Uri iconUri) {
   MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
   mediaDescriptionBuilder.setMediaId(mediaId);
   mediaDescriptionBuilder.setTitle(folderName);
   mediaDescriptionBuilder.setIconUri(iconUri);
   Bundle extras = new Bundle();
   extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs");
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), /* playable or browsable flag*/);
}

Your app must pass all of the media items that you want to group together as a contiguous block. For example, suppose that you wanted to display two groups of media items, "Songs" and "Albums" (in that order), and your app passed in five media items in the following order:

  1. Media item A with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  2. Media item B with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")
  3. Media item C with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  4. Media item D with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  5. Media item E with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")

Because the media items for the "Songs" group and "Albums" group are not kept together in contiguous blocks, Android Auto and Android Automotive OS would interpret this as the following four groups instead:

  • Group 1 called "Songs" containing media item A
  • Group 2 called "Albums" containing media item B
  • Group 3 called "Songs" containing media items C and D
  • Group 4 called "Albums" containing media item E

To display these items in two groups, your app would pass the apps in the following order instead:

  1. Media item A with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  2. Media item C with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  3. Media item D with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  4. Media item B with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")
  5. Media item E with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")

Display additional metadata indicators

Figure 3. Playback view with metadata indicators

You can include additional metadata indicators to provide at-a-glance information for content in the media browser tree and during playback. Within the browse tree, Android Auto and Android Automotive OS read the extras associated with an item and looks for certain constants to determine which indicators to display. During media playback, Android Auto and Android Automotive OS read the metadata for the media session and look for certain constants to determine indicators to display.

Use the following code to declare metadata indicator constants in your app:

Kotlin

// Bundle extra indicating that a song contains explicit content.
var EXTRA_IS_EXPLICIT = "android.media.IS_EXPLICIT"

/**
* Bundle extra indicating that a media item is available offline.
* Same as MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS.
*/
var EXTRA_IS_DOWNLOADED = "android.media.extra.DOWNLOAD_STATUS"

/**
* Bundle extra value indicating that an item should show the corresponding
* metadata.
*/
var EXTRA_METADATA_ENABLED_VALUE:Long = 1

/**
* Bundle extra indicating the played state of long-form content (such as podcast
* episodes or audiobooks).
*/
var EXTRA_PLAY_COMPLETION_STATE = "android.media.extra.PLAYBACK_STATUS"

/**
* Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
* not been played at all.
*/
var STATUS_NOT_PLAYED = 0

/**
* Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
* been partially played (i.e. the current position is somewhere in the middle).
*/
var STATUS_PARTIALLY_PLAYED = 1

/**
* Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
* been completed.
*/
var STATUS_FULLY_PLAYED = 2

Java

// Bundle extra indicating that a song contains explicit content.
String EXTRA_IS_EXPLICIT = "android.media.IS_EXPLICIT";

/**
 * Bundle extra indicating that a media item is available offline.
 * Same as MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS.
 */
String EXTRA_IS_DOWNLOADED = "android.media.extra.DOWNLOAD_STATUS";

/**
 * Bundle extra value indicating that an item should show the corresponding
 * metadata.
 */
long EXTRA_METADATA_ENABLED_VALUE = 1;

/**
 * Bundle extra indicating the played state of long-form content (such as podcast
 * episodes or audiobooks).
 */
String EXTRA_PLAY_COMPLETION_STATE = "android.media.extra.PLAYBACK_STATUS";

/**
 * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
 * not been played at all.
 */
int STATUS_NOT_PLAYED = 0;

/**
 * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
 * been partially played (i.e. the current position is somewhere in the middle).
 */
int STATUS_PARTIALLY_PLAYED = 1;

/**
 * Value for EXTRA_PLAY_COMPLETION_STATE that indicates the media item has
 * been completed.
 */
int STATUS_FULLY_PLAYED = 2;

After you have declared these constants, you can use them to display metadata indicators. To display indicators that appear while the user is browsing the media browser tree, create an extras bundle that includes one or more of these constants and pass that bundle to the MediaDescription.Builder.setExtras() method.

The following code snippet shows how to display indicators for an explicit, partially-played media item:

Kotlin

val extras = Bundle()
extras.putLong(EXTRA_IS_EXPLICIT, 1)
extras.putInt(EXTRA_PLAY_COMPLETION_STATE, STATUS_PARTIALLY_PLAYED)
val description = MediaDescriptionCompat.Builder()
.setMediaId(/*...*/)
.setTitle(resources.getString(/*...*/))
.setExtras(extras)
.build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

Java

Bundle extras = new Bundle();
extras.putLong(EXTRA_IS_EXPLICIT, 1);
extras.putInt(EXTRA_PLAY_COMPLETION_STATE, STATUS_PARTIALLY_PLAYED);

MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
  .setMediaId(/*...*/)
  .setTitle(resources.getString(/*...*/))
  .setExtras(extras)
  .build();
return new MediaBrowserCompat.MediaItem(description, /* flags */);

To display indicators for a media item that is currently being played, you can declare Long values for EXTRA_IS_EXPLICIT or EXTRA_IS_DOWNLOADED in the MediaMetadata.Builder() method of your mediaSession. You cannot display the EXTRA_PLAY_COMPLETION_STATE indicator on the playback view.

The following code snippet shows how to indicate that the current song in the playback view is explicit and downloaded:

Kotlin

mediaSession.setMetadata(
  MediaMetadata.Builder()
  .putString(
    MediaMetadata.METADATA_KEY_DISPLAY_TITLE, "Song Name")
  .putString(
    MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
  .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArtUri.toString())
  .putLong(
    EXTRA_IS_EXPLICIT, EXTRA_METADATA_ENABLED_VALUE)
  .putLong(
    EXTRA_IS_DOWNLOADED, EXTRA_METADATA_ENABLED_VALUE)
  .build())

Java

mediaSession.setMetadata(
    new MediaMetadata.Builder()
        .putString(
            MediaMetadata.METADATA_KEY_DISPLAY_TITLE, "Song Name")
        .putString(
            MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
        .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArtUri.toString())
        .putLong(
            EXTRA_IS_EXPLICIT, EXTRA_METADATA_ENABLED_VALUE)
        .putLong(
            EXTRA_IS_DOWNLOADED, EXTRA_METADATA_ENABLED_VALUE)
        .build());

To help a user browse your content, your app can allow a user to browse a group of search results that are related to their search query whenever that user performs a voice search. Android Auto and Android Automotive OS display these results as a "Show more results" bar in the interface.

Figure 4. Show more results option on the car screen

To display browsable search results, you should create a constant and include that constant in the extras bundle of your service's onGetRoot() method.

The following code snippet shows how to enable support in the onGetRoot() method:

Kotlin

// Bundle extra indicating that onSearch() is supported
val EXTRA_MEDIA_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED"

@Nullable
fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putBoolean(EXTRA_MEDIA_SEARCH_SUPPORTED, true)
    return BrowserRoot(ROOT_ID, extras)
}

Java

public static final String EXTRA_MEDIA_SEARCH_SUPPORTED =
   "android.media.browse.SEARCH_SUPPORTED";

@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
   @Nullable Bundle rootHints) {
   Bundle extras = new Bundle();
   extras.putBoolean(EXTRA_MEDIA_SEARCH_SUPPORTED, true);
   return new BrowserRoot(ROOT_ID, extras);
}

To start providing search results, override the onSearch() method in your media browser service. Android Auto and Android Automotive OS forward a user's search terms to this method whenever a user invokes the “Show more results” affordance. You can organize the search results from your service's onSearch() method using title items to make them more browsable. For example, if your app plays music, you might organize search results by “Album”, “Artist”, and “Songs”.

The following code snippet shows a simple implementation of the onSearch() method:

Kotlin

fun onSearch(query: String, extras: Bundle) {
  // Detach from results to unblock the caller (if a search is expensive)
  result.detach()
  object:AsyncTask() {
    internal var searchResponse:ArrayList
    internal var succeeded = false
    protected fun doInBackground(vararg params:Void):Void {
      searchResponse = ArrayList()
      if (doSearch(query, extras, searchResponse))
      {
        succeeded = true
      }
      return null
    }
    protected fun onPostExecute(param:Void) {
      if (succeeded)
      {
        // Sending an empty List informs the caller that there were no results.
        result.sendResult(searchResponse)
      }
      else
      {
        // This invokes onError() on the search callback
        result.sendResult(null)
      }
      return null
    }
  }.execute()
}
// Populates resultsToFill with search results. Returns true on success or false on error
private fun doSearch(
    query: String,
    extras: Bundle,
    resultsToFill: ArrayList
): Boolean {
  // Implement this method
}

Java

@Override
public void onSearch(final String query, final Bundle extras,
                        Result<ArrayList<MediaItem>> result) {

  // Detach from results to unblock the caller (if a search is expensive)
  result.detach();

  new AsyncTask<Void, Void, Void>() {
    ArrayList<MediaItem> searchResponse;
    boolean succeeded = false;
    @Override
    protected Void doInBackground(Void... params) {
      searchResponse = new ArrayList<MediaItem>();
      if (doSearch(query, extras, searchResponse)) {
        succeeded = true;
      }
      return null;
    }

    @Override
    protected void onPostExecute(Void param) {
      if (succeeded) {
       // Sending an empty List informs the caller that there were no results.
       result.sendResult(searchResponse);
      } else {
        // This invokes onError() on the search callback
        result.sendResult(null);
      }
      return null;
    }
  }.execute()
}

/** Populates resultsToFill with search results. Returns true on success or false on error */
private boolean doSearch(String query, Bundle extras, ArrayList<MediaItem> resultsToFill) {
    // Implement this method
}

Enable playback control

Android Auto and Android Automotive OS send playback control commands through your service's MediaSessionCompat. You must register a session and implement the associated callback methods.

Register a media session

In your media browser service's onCreate() method, create a MediaSessionCompat, then register the media session by calling setSessionToken().

The following code snippet shows how to create and register a media session:

Kotlin

override fun onCreate() {
    super.onCreate()

    ...
    // Start a new MediaSession
    val session = MediaSessionCompat(this, "session tag").apply {
        // Set a callback object to handle play control requests, which
        // implements MediaSession.Callback
        setCallback(MyMediaSessionCallback())
    }
    sessionToken = session.sessionToken

    ...
}

Java

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

    ...
    // Start a new MediaSession
    MediaSessionCompat session = new MediaSessionCompat(this, "session tag");
    setSessionToken(session.getSessionToken());

    // Set a callback object to handle play control requests, which
    // implements MediaSession.Callback
    session.setCallback(new MyMediaSessionCallback());

    ...
}

When you create the media session object, you set a callback object that is used to handle playback control requests. You create this callback object by providing an implementation of the MediaSessionCompat.Callback class for your app. The next section discusses how to implement this object.

Implement play commands

When a user requests playback for a media item from your app, Android Automotive OS and Android Auto use the MediaSessionCompat.Callback class from your app's MediaSessionCompat object that they obtained from your app's media browser service. When a user wants to control content playback, such as pausing playback or skipping to the next track, Android Auto and Android Automotive OS invoke one of the callback object's methods.

To handle content playback, your app must extend the abstract MediaSessionCompat.Callback class and implement the methods that your app supports.

You should implement all of the following callback methods that make sense for the type of content that your app offers:

onPrepare()
Invoked when the media source is changed. Android Automotive OS also invokes this method immediately after booting. Your media app must implement this method.
onPlay()
Invoked if the user chooses play without choosing a specific item. Your app should play its default content. If playback was paused with onPause(), your app should resume playback.

Note: Your app should not automatically start playing music when Android Automotive OS or Android Auto connect to your media browser service. For more information, see Set initial playback state.

onPlayFromMediaId()
Invoked when the user chooses to play a specific item. The method is passed the ID that your media browser service assigned to the media item in your content hierarchy.
onPlayFromSearch()
Invoked when the user chooses to play from a search query. The app should make an appropriate choice based on the search string that was passed in.
onPause()
Invoked when the user chooses to pause playback.
onSkipToNext()
Invoked when the user chooses to skip to the next item.
onSkipToPrevious()
Invoked when the user chooses to skip to the previous item.
onStop()
Invoked when the user chooses to stop playback.

Your app should override these methods to provide any desired functionality. You don't need to implement a method if it is not supported by your app. For example, if your app plays a live stream (such as a sports broadcast), the onSkipToNext() method would not make sense to implement, and you could use the default implementation of onSkipToNext() instead.

Your app does not need any special logic to play content through the car's speakers. When your app receives a request to play content, it should play audio the same way it would normally (for example, playing the content through a user's phone speakers or headphones). Android Auto and Android Automotive OS automatically send the audio content to the car's system to play over the car's speakers.

For more information about playing audio content, see Media playback, Managing audio playback, and ExoPlayer.

Set standard playback actions

Android Auto and Android Automotive OS display playback controls based on the actions that are enabled in the PlaybackStateCompat object.

By default, your app must support the following actions:

In addition, you might want to create a play queue that can be displayed for the user. To do this, you need to call the setQueue() and setQueueTitle() methods, enable the ACTION_SKIP_TO_QUEUE_ITEM action, and define the callback onSkipToQueueItem().

Android Auto and Android Automotive OS display buttons for each enabled action, as well as the playback queue if you choose to create one.

Reserve unused space

Android Auto and Android Automotive OS reserve space in the UI for the ACTION_SKIP_TO_PREVIOUS and ACTION_SKIP_TO_NEXT actions. In addition, Android Auto reserves space for the playback queue. If your app does not support one of these functions, Android Auto and Android Automotive OS use the space to display any custom actions you create.

If you don't want to fill those spaces with custom actions, you can reserve them so that Android Auto and Android Automotive OS will leave the space blank whenever your app does not support the corresponding function. To do this, call the setExtras() method with an extras bundle that contains constants that correspond to each of the reserved functions. Set each constant that you want to reserve space for to true.

The following code snippet shows the constants that you can use to reserve unused space:

Kotlin

// Use these extras to show the transport control buttons for the corresponding actions,
// even when they are not enabled in the PlaybackState.
private const val SLOT_RESERVATION_SKIP_TO_NEXT =
        "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"
private const val SLOT_RESERVATION_SKIP_TO_PREV =
        "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"
private const val SLOT_RESERVATION_QUEUE =
        "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE"

Java

// Use these extras to show the transport control buttons for the corresponding actions,
// even when they are not enabled in the PlaybackState.
private static final String SLOT_RESERVATION_SKIP_TO_NEXT =
    "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
private static final String SLOT_RESERVATION_SKIP_TO_PREV =
    "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
private static final String SLOT_RESERVATION_QUEUE =
    "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";

Set initial PlaybackState

As Android Auto and Android Automotive OS communicate with your media browser service, your media session communicates the status of content playback using the PlaybackState. Your app should not automatically start playing music when Android Automotive OS or Android Auto connect to your media browser service. Instead, rely on Android Auto and Android Automotive OS to resume or start playback based on the car's state or user actions.

To accomplish this, set the initial PlaybackState of your media session to STATE_STOPPED, STATE_PAUSED, STATE_NONE, or STATE_ERROR.

Add custom playback actions

You can add custom playback actions to display additional actions that your media app supports. If space permits (and is not reserved), Android adds the custom actions to the transport controls. Otherwise, the custom actions are displayed in the overflow menu. Custom actions display in the order in which they are added to the PlaybackState.

You can add these actions using the addCustomAction() method in the PlaybackStateCompat.Builder class.

The following code snippet shows how to add a custom “Start a radio channel” action:

Kotlin

stateBuilder.addCustomAction(
        PlaybackStateCompat.CustomAction.Builder(
                CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
                resources.getString(R.string.start_radio_from_media),
                startRadioFromMediaIcon
        ).run {
            setExtras(customActionExtras)
            build()
        }
)

Java

stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder(
    CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
    resources.getString(R.string.start_radio_from_media), startRadioFromMediaIcon)
    .setExtras(customActionExtras)
    .build());

For a more detailed example of this method, see the setCustomAction() method in the Universal Android Music Player sample app on GitHub.

After creating your custom action, your media session can respond to the action by overriding the onCustomAction() method.

The following code snippet shows how your app could respond to a “Start a radio channel” action:

Kotlin

override fun onCustomAction(action: String, extras: Bundle?) {
    when(action) {
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA -> {
            ...
        }
    }
}

Java

@Override
public void onCustomAction(@NonNull String action, Bundle extras) {
    if (CUSTOM_ACTION_START_RADIO_FROM_MEDIA.equals(action)) {
        ...
    }
}

For a more detailed example of this method, see the onCustomAction method in the Universal Android Music Player sample app on GitHub.

Icons for custom actions

Each custom action that you create requires an icon resource. Apps in cars can run on many different screen sizes and densities, so icons that you provide must be vector drawables. A vector drawable allows you to scale assets without losing the detail. A vector drawable also makes it easy to align edges and corners to pixel boundaries at smaller resolutions.

Provide alternative icon style for disabled actions

For cases when a custom action is unavailable for the current context, swap the custom action icon with an alternative icon that shows that the action is disabled.

Figure 5. Sample off style custom action icons.

Support voice actions

Your media app must support voice actions to help provide drivers with a safe and convenient experience that minimizes distractions. For example, if your app is already playing one media item, the user can say “Play Bohemian Rhapsody" to tell your app to play a different song without looking at or touching the car's display.

For a more detailed example on how to implement voice-enabled playback actions in your app, see The Google Assistant and media apps.

Implement distraction safeguards

Because a user's phone is connected to their car's speakers while using Android Auto, you must take additional precautions to help prevent driver distraction.

Detect car mode

Android Auto media apps must not start playing audio through the car speakers unless the user consciously starts playback (for example, by pressing play in your app). Even a user-scheduled alarm from your media app must not start playing music through the car speakers. To fulfill this requirement, your app should determine if the phone is in car mode before playing any audio. Your app can check whether the phone is in car mode by calling the getCurrentModeType() method.

If the user's phone is in car mode, media apps that support alarms must do one of the following things:

  • Disable the alarm.
  • Play the alarm over STREAM_ALARM, and provide a UI on the phone screen to disable the alarm.

The following code snippet shows how to check whether an app is running in car mode:

Kotlin

fun isCarUiMode(c: Context): Boolean {
    val uiModeManager = c.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
    return if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) {
        LogHelper.d(TAG, "Running in Car mode")
        true
    } else {
        LogHelper.d(TAG, "Running in a non-Car mode")
        false
    }
}

Java

 public static boolean isCarUiMode(Context c) {
      UiModeManager uiModeManager = (UiModeManager) c.getSystemService(Context.UI_MODE_SERVICE);
      if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
            LogHelper.d(TAG, "Running in Car mode");
            return true;
      } else {
          LogHelper.d(TAG, "Running in a non-Car mode");
          return false;
        }
  }

Handle media advertisements

By default, Android Auto displays a notification when the media metadata changes during an audio playback session. When a media app switches from playing music to running an advertisement, it is distracting (and unnecessary) to display a notification to the user. To prevent Android Auto from displaying a notification in this case, you must set the media metadata key android.media.metadata.ADVERTISEMENT to 1, as shown in the following code snippet:

Kotlin

const val EXTRA_METADATA_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT"
...
override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) {
    MediaMetadataCompat.Builder().apply {
        // ...
        if (isAd(mediaId)) {
            putLong(EXTRA_METADATA_ADVERTISEMENT, 1)
        }
        // ...
        mediaSession.setMetadata(build())
    }
}

Java

public static final String EXTRA_METADATA_ADVERTISEMENT =
    "android.media.metadata.ADVERTISEMENT";

@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
    MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
    // ...
    if (isAd(mediaId)) {
        builder.putLong(EXTRA_METADATA_ADVERTISEMENT, 1);
    }
    // ...
    mediaSession.setMetadata(builder.build());
}

Handle general errors

When the app experiences an error, you should set the playback state to STATE_ERROR and provide an error message using the setErrorMessage() method. Error messages must be user-facing and localized with the user's current locale. Android Auto and Android Automotive OS can then display the error message to the user.

For more information about error states, see Working with a media session: States and errors.

If an Android Auto user needs to open your phone app to resolve an error, your message should provide that information to the user. For example, your error message would say "Sign in to [your app name]" instead of "Please sign in".

Other resources