Build Android media apps for cars

Android Automotive OS and Android Auto help you bring your media app content to users in their car. For an overview of how Android enables app experiences on the road, see Android for Cars overview.

This guide assumes that you already have a media phone app. This guide describes how to build an app for Android Automotive OS and how to extend your phone app for Android Auto.

Before you begin

Before you begin building your app, make sure to follow the steps in Get started with Android for Cars, and then review the information in this section.

Key terms and concepts

Media Browse Service
An Android service implemented by your media app that complies with the MediaBrowseServiceCompat API. Your app uses this service to expose Media Browse content to Android Automotive OS and Android Auto.
Media Browse
An API used by media apps to expose their content to Android Automotive OS and Android Auto.
Media Item
An individual MediaBrowserCompat.MediaItem object in the Media Browse tree. Media items can be one of these types:
  • Playable: These items represent actual sound streams such as songs from an album, chapters from a book, or episodes from a podcast.
  • Browseable: These items organize playable media items into groups. For example, you can group chapters into a book, songs into an album, or episodes into a podcast.

Note: A media item that is both browsable and playable is treated as playable.

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 Browse service.

Configure your app's manifest files

You need to configure your app's manifest files to indicate that your app is available for Android Automotive OS and that your phone app supports media services for Android Auto.

Declare support for Android Automotive OS

The app that you distribute to Android Automotive OS must be separate from your phone app. We recommend using modules and an Android App Bundle to help you reuse code and build and release your app with ease. Add the following entry into the manifest file in your Android Automotive OS module to indicate that the module's code is restricted to Android Automotive OS:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.media">
   <uses-feature
           android:name="android.hardware.type.automotive"
           android:required="true"/>
</manifest>

Declare media support for Android Auto

Android Auto uses your phone app to provide a driver-optimized experience for a user. Use the following manifest entry to declare that your phone app supports Android Auto:

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

This manifest entry refers to an XML file that declares what automotive capabilities your app supports. To indicate that you have a media app, add an XML file named automotive_app_desc.xml to the res/xml/ directory in your project. This file should include the following content:

<automotiveApp>
    <uses name="media"/>
</automotiveApp>

Declare your Media Browse service

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

The following code snippet shows how to declare your Media Browse 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

Android Automotive OS and Android Auto display your app's icon to users in different places as they use your app and interact with their car. For example, if a user has a navigation app running, and one song finishes and a new song starts, the user would see a notification that includes your app's icon. Android Auto and Android Automotive OS also display your app's icon in other locations as the user browses your media content.

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>

Build a Media Browse service

You create a Media Browse service by extending the MediaBrowserServiceCompat class. Both Android Automotive OS and Android Auto 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 Browse 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 Browse 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 browseable.
  5. If the user selects a browseable 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.

How users browse within media apps

To help users quickly browse your app's content, Android Auto includes a browsing capability that lets users select a letter from an on-screen keyboard. The user is then presented with a list of items beginning with that letter in the current drawer list. This works on both sorted and unsorted content and is currently available only in English.

Figure 1. Alpha picker on car screen

Figure 2. Alphabetical list view on car screen

Build your content hierarchy

Android Automotive OS and Android Auto call your app's Media Browse service to find out what content is available. You need to implement two methods in your 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 Automotive OS and Android Auto 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 Automotive OS and Android Auto) 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 Automotive OS and Android Auto 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 Automotive OS and Android Auto 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 browseable 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.

Title items

This content style displays even more metadata than the List Items content style. To use this content style, additional metadata must be provided for each media item.

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 Automotive OS and Android Auto 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)
    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);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}

Add title items

To present your media items as title items, you use a per-item content style that groups the items together. Every media item in the 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.

Android Automotive OS and Android Auto don't sort items that are grouped in this way. You need to pass the media items together and in the order that you want them to be displayed.

For example, suppose that your app passed three media items in the following order:

  • Media item 1 with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  • Media item 2 with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")
  • Media item 3 with extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")

Android Automotive OS and Android Auto would not merge media item 1 and media item 3 into a group, "Songs". Instead, the media items would be kept separate.

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")
    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");
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), /* playable or browsable flag*/);
}

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 Browse tree and during playback. Within the browse tree, Android Automotive OS and Android Auto read the extras associated with an item and looks for certain constants to determine which indicators to display. During media playback, Android Automotive OS and Android Auto 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 Browse 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 Automotive OS and Android Auto display these results as a "Show more results" bar in the interface.

Figure 4. Show more results option on the car screen

To display browseable 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 Browse service. Android Automotive OS and Android Auto 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 browseable. 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 Automotive OS and Android Auto 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 Browse 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 Browse service. When a user wants to control content playback, such as pausing playback or skipping to the next track, Android Automotive OS and Android Auto 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 Browse 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 Browse 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 Automotive OS and Android Auto 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 Automotive OS and Android Auto 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 Automotive OS and Android Auto display buttons for each enabled action, as well as the playback queue if you choose to create one.

Reserve unused space

Android Automotive OS and Android Auto 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 Automotive OS and Android Auto 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 Automotive OS and Android Auto 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 Automotive OS and Android Auto communicate with your Media Browse 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 Browse service. Instead, rely on Android Automotive OS and Android Auto 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 [item]" to tell your app to play a different item without looking at or touching the car's display.

Declare support for voice actions

The following code snippet shows how to declare support for voice actions in your app's manifest files. You should include this code in the manifest file for your Android Automotive OS module and in the manifest file for your phone app.

<activity>
    <intent-filter>
        <action android:name=
             "android.media.action.MEDIA_PLAY_FROM_SEARCH" />
        <category android:name=
             "android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

Parse voice search queries

When a user searches for a specific media item, such as “Play jazz on [your app name]” or “Listen to [song title]”, the onPlayFromSearch() callback method receives the voice search results in the query parameter and an extras bundle.

Your app can parse the voice search query and start playback by following these steps:

  1. Use the extras bundle and search query string returned from the voice search to filter results.
  2. Build a playback queue based on these results.
  3. Play the most relevant media item from the results.

The onPlayFromSearch() method takes an extras parameter with more detailed information from the voice search. These extras help you find the audio content in your app for playback. If the search results are unable to provide this data, you can implement logic to parse the raw search query and play the appropriate tracks based on the query.

The following extras are supported in Android Automotive OS and Android Auto:

The following code snippet shows how to override the onPlayFromSearch() method in your MediaSession.Callback implementation to parse the voice search query and begin playback:

Kotlin

override fun onPlayFromSearch(query: String?, extras: Bundle?) {
    if (query.isNullOrEmpty()) {
        // The user provided generic string e.g. 'Play music'
        // Build appropriate playlist queue
    } else {
        // Build a queue based on songs that match "query" or "extras" param
        val mediaFocus: String? = extras?.getString(MediaStore.EXTRA_MEDIA_FOCUS)
        if (mediaFocus == MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE) {
            isArtistFocus = true
            artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)
        } else if (mediaFocus == MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE) {
            isAlbumFocus = true
            album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)
        }

        // Implement additional "extras" param filtering
    }

    // Implement your logic to retrieve the queue
    var result: String? = when {
        isArtistFocus -> artist?.also {
            searchMusicByArtist(it)
        }
        isAlbumFocus -> album?.also {
            searchMusicByAlbum(it)
        }
        else -> null
    }
    result = result ?: run {
        // No focus found, search by query for song title
        query?.also {
            searchMusicBySongTitle(it)
        }
    }

    if (result?.isNotEmpty() == true) {
        // Immediately start playing from the beginning of the search results
        // Implement your logic to start playing music
        playMusic(result)
    } else {
        // Handle no queue found. Stop playing if the app
        // is currently playing a song
    }
}

Java

@Override
public void onPlayFromSearch(String query, Bundle extras) {
    if (TextUtils.isEmpty(query)) {
        // The user provided generic string e.g. 'Play music'
        // Build appropriate playlist queue
    } else {
        // Build a queue based on songs that match "query" or "extras" param
        String mediaFocus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS);
        if (TextUtils.equals(mediaFocus,
                MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE)) {
            isArtistFocus = true;
            artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST);
        } else if (TextUtils.equals(mediaFocus,
                MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE)) {
            isAlbumFocus = true;
            album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM);
        }

        // Implement additional "extras" param filtering
    }

    // Implement your logic to retrieve the queue
    if (isArtistFocus) {
        result = searchMusicByArtist(artist);
    } else if (isAlbumFocus) {
        result = searchMusicByAlbum(album);
    }

    if (result == null) {
        // No focus found, search by query for song title
        result = searchMusicBySongTitle(query);
    }

    if (result != null && !result.isEmpty()) {
        // Immediately start playing from the beginning of the search results
        // Implement your logic to start playing music
        playMusic(result);
    } else {
        // Handle no queue found. Stop playing if the app
        // is currently playing a song
    }
}

For a more detailed example on how to implement voice search to play audio content in your app, see the Universal Media Player sample.

Handle empty queries

When a user says “Play music on [your app name]”, Android Automotive OS or Android Auto attempts to launch your app and play audio by calling your app's onPlayFromSearch() method. However, because the user did not say the name of media item, the onPlayFromSearch() method receives an empty query parameter. In these cases, your app should respond by immediately playing audio, such as a song from the most recent playlist or a random queue.

Implement voice-enabled playback actions

To provide a hands-free experience while users drive and listen to media content, your app must allow users to control content playback with voice actions. When users speak commands such as “Next song”, “Pause music”, or “Resume music”, the system triggers the corresponding callback method where you implement the playback control action.

To provide voice-enabled playback actions, first enable the hardware controls by setting these flags in your app's MediaSession object:

Kotlin

session.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
        or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)

Java

session.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
    MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

After setting the flags, implement the callback methods with the playback controls that you support in your app. Android Automotive OS and Android Auto support the following voice-enabled playback actions:

Example phrase Callback method
"Next song" onSkipToNext()
"Previous song" onSkipToPrevious()
"Pause music" onPause()
"Stop music" onStop()
"Resume music" onPlay()

For a more detailed example on how to implement voice-enabled playback actions in your app, see the Universal Media Player sample.

Implement settings and sign-in activities for Android Automotive OS

In addition to your media browser service, you can also provide vehicle-optimized Settings and Sign-in activities for your Android Automotive OS app. These activities allow you provide app functionality that isn't included in the Android Media APIs.

Add a Settings activity

You can add a vehicle-optimized Settings activity so that users can configure settings for your app in their car. Your settings activity can also provide other workflows, like signing in or out of a user's account or switching user accounts.

Settings activity workflows

Your Settings activity can provide the user with different workflows. The following image shows how a user interacts with your Settings activity using Android Automotive OS:

Workflows for a Settings activity

Figure 6. Diagram of workflows for a Settings activity

Declare a Settings activity

You must declare your Settings activity in your app's manifest file, as shown in the following code snippet:

<application>
    ...
    <activity android:name=".AppSettingsActivity"
              android:theme="@style/SettingsActivity"
              android:label="@string/app_settings_activity_title">
        <intent-filter>
            <action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
        </intent-filter>
    </activity>
    ...
<application>

Implement your Settings activity

When a user launches your app, Android Automotive OS detects the Settings activity that you declared and displays an affordance. The user can tap or select this affordance using their car's display to navigate to the activity. Android Automotive OS sends the ACTION_APPLICATION_PREFERENCES intent that tells your app to start your settings activity.

Add a Sign-in activity

If your app requires a user to sign in before they can use your app, you can add a vehicle-optimized Sign-in activity that handles signing in and out of your app. You can also add sign-in and sign-out workflows to a Settings activity, but you should use a dedicated Sign-in activity if your app cannot be used until a user signs in.

Sign-in activity workflow

The following image shows how a user interacts with your Sign-in activity using Android Automotive OS:

Workflows for a Sign-in activity

Figure 7. Diagram of workflows for a Sign-in activity

Require sign in at app start

To require a user to sign in using your Sign-in activity before they can use your app, your media browse service must do the following things:

  1. Set the media session's PlaybackState to STATE_ERROR using the setState() method. This tells Android Automotive OS that no other operations can be performed until the error has been resolved.
  2. Set the media session's PlaybackState error code to ERROR_CODE_AUTHENTICATION_EXPIRED. This tells Android Automotive OS that the user needs to authenticate.
  3. Set the media session's PlaybackState error message using the setErrorMessage() method. Because this error message is user-facing, the message must be localized for the user's current locale.
  4. Set the media session's PlaybackState extras using the setExtras() method. Include the following two keys:

    • android.media.extras.ERROR_RESOLUTION_ACTION_LABEL: A string that is displayed on the button that begins the sign-in workflow. Because this string is user-facing, it must be localized for the user's current locale.
    • android.media.extras.ERROR_RESOLUTION_ACTION_INTENT: A PendingIntent that directs the user to your Sign-in activity when the user taps the button referred to by the android.media.extras.ERROR_RESOLUTION_ACTION_LABEL.

The following code snippet shows how your app can require the user to sign in before using your app:

Kotlin

val signInIntent = Intent(this, SignInActivity::class.java)
val signInActivityPendingIntent = PendingIntent.getActivity(this, 0,
    signInIntent, 0)
val extras = Bundle().apply {
    putString(
        "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL",
        "Sign in"
    )
    putParcelable(
        "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT",
        signInActivityPendingIntent
    )
}

val playbackState = PlaybackStateCompat.Builder()
        .setState(PlaybackStateCompat.STATE_ERROR, 0, 0f)
        .setErrorMessage(
            PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
            "Authentication required"
        )
        .setExtras(extras)
        .build()
mediaSession.setPlaybackState(playbackState)

Java

Intent signInIntent = new Intent(this, SignInActivity.class);
PendingIntent signInActivityPendingIntent = PendingIntent.getActivity(this, 0,
    signInIntent, 0);
Bundle extras = new Bundle();
extras.putString(
    "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL",
    "Sign in");
extras.putParcelable(
    "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT",
    signInActivityPendingIntent);

PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
    .setState(PlaybackStateCompat.STATE_ERROR, 0, 0f)
    .setErrorMessage(
            PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
            "Authentication required"
    )
    .setExtras(extras)
    .build();
mediaSession.setPlaybackState(playbackState);

After the user has successfully authenticated, your app must set the PlaybackState back to a state other than STATE_ERROR and then take the user back to Android Automotive OS by calling the activity's finish() method.

Implement your Sign-in activity

Google offers a variety of identity tools that you can use to help users sign in to your app in their cars. Some tools, such as Firebase Authentication, provide full-stack toolkits that can help you build customized authentication experiences. Other tools leverage a user's existing credentials or other technologies to help you build seamless sign-in experiences for users.

We recommend the following tools to help you build an easier sign-in experience for users that have previously signed in on another device:

  • Google Sign-in: If you've already implemented Google Sign-in for other devices (such as your phone app), you should also implement Google Sign-in for your Android Automotive OS app to support existing Google Sign-in users.
  • Autofill with Google: If users have opted into Autofill with Google on their other Android devices, their credentials are saved to the Google password manager. Then, when the user signs in to your Android Automotive OS app, Autofill with Google suggests relevant saved credentials. Using Autofill with Google requires no application development effort; however, application developers should optimize their apps for better quality results. Autofill with Google is supported by all devices running Android Oreo 8.0 (API level 26) or higher (including Android Automotive OS).

Handle sign-in protected actions

Some apps allow a user to access some actions anonymously, but require the user to sign in before they can perform other actions. For example, a user might be able to play music in an app before signing in, but they must sign in before they can skip a song.

In this case, when the user attempts to perform the restricted action (skipping a song), your app could suggest that user authenticate by issuing a non-fatal error. By using a non-fatal error, the system displays the message to the user without interrupting playback for the current media item. To implement non-fatal error handling, complete the following steps:

  1. Set the errorCode for the media session's PlaybackState to ERROR_CODE_AUTHENTICATION_EXPIRED. This tells Android Automotive OS that user needs to authenticate.
  2. Keep the state for the media session's PlaybackState as is—don't set it to STATE_ERROR. This is what tells the system that the error is non-fatal.
  3. Set the media session's PlaybackState extras using the setExtras() method. Include the following two keys:

    • android.media.extras.ERROR_RESOLUTION_ACTION_LABEL: A string that is displayed on the button that begins the sign-in workflow. Because this string is user-facing, it must be localized for the user's current locale.
    • android.media.extras.ERROR_RESOLUTION_ACTION_INTENT: A PendingIntent that directs the user to your Sign-in activity when the user taps the button referred to by the android.media.extras.ERROR_RESOLUTION_ACTION_LABEL.
  4. Retain the rest of the media session's PlaybackState state as is. This allows playback for the current media item to continue while the user decides whether to sign in or not.

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 Automotive OS and Android Auto 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