构建车载媒体应用

Android Auto 和 Android Automotive OS 可帮助您向用户提供车载媒体应用内容。车载媒体应用必须提供媒体浏览器服务,Android Auto 和 Android Automotive OS 或其他带有媒体浏览器的应用才能发现和展示您的内容。

本指南假定您已经拥有可在手机上播放音频的媒体应用,并且您的媒体应用遵循 Android 媒体应用架构

本指南介绍了您的应用在 Android Auto 或 Android Automotive OS 上运行所需的 MediaBrowserServiceMediaSession 组件。完成核心媒体基础架构的搭建后,您可以向媒体应用添加对 Android Auto 的支持添加对 Android Automotive OS 的支持

准备工作

  1. 查看 Android 媒体 API 文档
  2. 查看 Android Automotive OS 应用设计准则Android Auto 应用设计准则
  3. 查看本部分列出的关键术语和概念。

关键术语和概念

媒体浏览器服务
由您的媒体应用实现且符合 MediaBrowserServiceCompat API 要求的 Android 服务。应用使用此服务来公开其内容。
媒体浏览器
媒体应用用于发现媒体浏览器服务并展示其内容的 API。Android Auto 和 Android Automotive OS 使用媒体浏览器查找应用的媒体浏览器服务。
媒体项

媒体浏览器将其内容整理成 MediaItem 对象树。媒体项可以包含以下两个标记之一,也可以同时包含这两个标记:

  • 可播放:此标记表示媒体项为内容树上的一个叶项。这类媒体项表示单个声音流,如专辑里的一首歌曲、有声读物中的一章或一集播客。
  • 可浏览:此标记表示媒体项为内容树上的一个节点,它有子项。例如,媒体项表示一个专辑,其子项为专辑里的歌曲。

既可浏览又可播放的媒体项相当于播放列表。您可以选择媒体项本身来播放其中的所有子项,也可以浏览其子项。

车辆优化

遵循 Android Automotive OS 设计准则的 Android Automotive OS 应用的 Activity。这些 Activity 的界面并非由 Android Automotive OS 绘制,因此您必须确保应用遵循设计准则。通常情况下,这包括使用较大的点按目标和字体大小、支持日间模式和夜间模式以及提高对比度。

只有在车载用户体验限制 (CUXR) 未生效时才允许显示车辆优化界面,因为这些界面可能需要用户长时间关注或互动。汽车处于停车状态时 CUXR 不起作用,但在汽车行驶时始终有效。

您无需针对 Android Auto 设计 Activity,因为 Android Auto 会使用媒体浏览器服务中的信息自行绘制车辆优化界面。

配置应用的清单文件

您需要先配置应用的清单文件,然后才能创建媒体浏览器服务。

声明媒体浏览器服务

Android Auto 和 Android Automotive OS 都通过媒体浏览器服务连接到您的应用,以便浏览媒体项。您需要在清单中声明媒体浏览器服务,以便 Android Auto 和 Android Automotive OS 发现该服务并连接到您的应用。

以下代码段展示了如何在清单中声明媒体浏览器服务。您应将此代码包含在 Android Automotive OS 模块的清单文件和手机应用的清单文件中。

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

指定应用图标

您需要指定一个应用图标,供 Android Auto 和 Android Automotive OS 用来在系统界面中表示您的应用。

您可以通过以下清单声明指定用于表示应用的图标:

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

创建媒体浏览器服务

请通过扩展 MediaBrowserServiceCompat 类创建媒体浏览器服务。然后,Android Auto 和 Android Automotive OS 可以使用您的服务执行以下操作:

  • 浏览应用的内容层次结构,以便向用户展示菜单。
  • 获取应用的 MediaSessionCompat 对象的令牌,以便控制音频播放。

媒体浏览器服务工作流

本部分介绍在典型的用户工作流中,Android Automotive OS 和 Android Auto 如何与您的媒体浏览器服务互动。

  1. 用户在 Android Automotive OS 或 Android Auto 中启动您的应用。
  2. Android Automotive OS 或 Android Auto 使用 onCreate() 方法与应用的媒体浏览器服务通讯。在 onCreate() 方法的实现中,您必须创建并注册 MediaSessionCompat 对象及其回调对象。
  3. Android Automotive OS 或 Android Auto 调用您的服务的 onGetRoot() 方法,以获取内容层次结构中的根媒体项。根媒体项不会显示出来,而是用于从您的应用中检索更多内容。
  4. Android Automotive OS 或 Android Auto 调用服务的 onLoadChildren() 方法,以获取根媒体项的子项。Android Automotive OS 和 Android Auto 会将这些媒体项显示为顶级内容项。顶级内容项应该是可浏览项。
  5. 如果用户选择了可浏览媒体项,系统会再次调用服务的 onLoadChildren() 方法,以检索所选菜单项的子项。
  6. 如果用户选择了可播放媒体项,Android Automotive OS 或 Android Auto 会调用相应的媒体会话回调方法来执行该操作。
  7. 如果您的应用支持的话,用户还可以搜索您的内容。在这种情况下,Android Automotive OS 或 Android Auto 会调用服务的 onSearch() 方法。

构建内容层次结构

Android Auto 和 Android Automotive OS 调用应用的媒体浏览器服务,以查找可用内容。为了支持这一点,您需要在媒体浏览器服务中实现 onGetRoot()onLoadChildren() 这两种方法。

实现 onGetRoot

服务的 onGetRoot() 方法返回有关内容层次结构根节点的信息。Android Auto 和 Android Automotive OS 使用此根节点,通过 onLoadChildren() 方法请求您的其余内容。

以下代码段展示了 onGetRoot() 方法的简单实现:

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);
    }
    

如需通过更详细的示例来了解此方法,请参阅 GitHub 上 Universal Android Music Player 示例应用中的 onGetRoot() 方法。

为 onGetRoot() 添加软件包验证

调用服务的 onGetRoot() 方法时,发出调用的软件包会将标识信息传递给您的服务。您的服务可以使用此信息来决定该软件包能否访问您的内容。例如,您可以对比 clientPackageName 与您的白名单,并验证用于签署软件包 APK 的证书,从而限制只有获批的软件包有权访问应用内容。如果无法验证软件包,则返回 null 以拒绝它访问您的内容。

为了允许系统应用(例如 Android Auto 和 Android Automotive OS)访问您的内容,当这些系统应用调用 onGetRoot() 方法时,您的服务应始终返回一个非 null BrowserRoot

以下代码段演示了您的服务如何验证发出调用的软件包是否为系统应用:

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
    }
    

此代码段摘录自 GitHub 上 Universal Android Music Player 示例应用中的 PackageValidator 类。请参阅该类,通过更详细的示例来了解如何为服务的 onGetRoot() 方法实现软件包验证。

实现 onLoadChildren()

收到根节点对象后,Android Auto 和 Android Automotive OS 会对根节点对象调用 onLoadChildren() 来获取其子节点,以构建顶级菜单。客户端应用使用子节点对象调用同一方法来构建子菜单。

内容层次结构中的每个节点都由一个 MediaBrowserCompat.MediaItem 对象表示。其中每一个媒体项都由一个唯一的 ID 字符串标识。客户端应用将这些 ID 字符串视为不透明令牌。当客户端应用想要浏览子菜单或播放媒体项时,它会传递这个令牌。您的应用负责将令牌与相应的媒体项关联。

注意:Android Auto 和 Android Automotive OS 对菜单中各层级可显示的媒体项数量有严格的限制。这些限制可最大限度地减少对驾驶员的干扰,同时帮助他们通过语音指令操作应用。如需了解详情,请参阅浏览内容详情Android Auto 应用抽屉

以下代码段演示了 onLoadChildren() 方法的简单实现:

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);
    }
    

如需查看此方法的完整示例,请参阅 GitHub 上 Universal Android Music Player 示例应用中的 onLoadChildren() 方法。

应用内容样式

使用可浏览项或可播放项构建内容层次结构后,您可以应用内容样式来确定这些项目在汽车中的显示方式。

您可以采用以下内容样式:

列表项

此内容样式重点显示标题和元数据,其次是图片。

网格项

在此内容样式下,图片的优先级高于标题和元数据。

设置默认内容样式

您可以通过在服务的 onGetRoot() 方法的 BrowserRoot extra 包中加入特定常量,设置媒体项的全局默认显示方式。Android Auto 和 Android Automotive OS 会读取与浏览树中各项关联的 extra 包,并查找这些常量来确定相应的样式。

请使用以下代码在应用中声明这些常量:

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;
    

声明这些常量后,请将它们添加到服务的 onGetRoot() 方法的 extra 包中,以设置默认内容样式。以下代码段演示了如何将可浏览项的默认内容样式设置为网格,并将可播放项的默认内容样式设置为列表:

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);
    }
    

按项设置内容样式

您可以通过 Content Style API,替换任何可浏览媒体项的子项的默认内容样式。如需替换默认设置,请在媒体项的 MediaDescription 中创建 extra 包。

以下代码段演示了如何创建替换默认内容样式的可浏览 MediaItem

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);
    }
    

使用标题提示对项进行分组

如需将相关媒体项归入一组,请使用按项提示。组中的每个媒体项都需要在其 MediaDescription 中声明一个使用相同字符串的 extra 包。此字符串将用作组标题,并且可以本地化。

以下代码段演示了如何创建子组标题为 "Songs"MediaItem

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*/);
    }
    

您的应用必须将您想要归入一组的所有媒体项传递为一个连续的块。例如,假设您希望显示“Songs”和“Albums”这两组媒体项(按此顺序),并且您的应用按以下顺序传递了五个媒体项:

  1. 媒体项 A,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  2. 媒体项 B,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")
  3. 媒体项 C,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  4. 媒体项 D,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  5. 媒体项 E,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")

由于“Songs”组和“Albums”组中的媒体项未归入连续的块中,因此 Android Auto 和 Android Automotive OS 会将其解读为以下四组:

  • 第 1 组名为“Songs”,包含媒体项 A
  • 第 2 组名为“Albums”,包含媒体项 B
  • 第 3 组名为“Songs”,包含媒体项 C 和 D
  • 第 4 组名为“Albums”,包含媒体项 E

如需按两组显示这些媒体项,您的应用应改为按以下顺序传递这些项:

  1. 媒体项 A,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  2. 媒体项 C,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  3. 媒体项 D,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  4. 媒体项 B,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")
  5. 媒体项 E,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")

显示其他元数据指示符

图 3. 包含用于识别歌曲和艺人的元数据的播放视图

您可以添加其他元数据指示符,以便在媒体浏览树和播放过程中提供一目了然的内容信息。在浏览树内,Android Auto 和 Android Automotive OS 会读取与项关联的 extra,并查找特定常量来确定要显示的指示符。在媒体播放期间,Android Auto 和 Android Automotive OS 会读取媒体会话的元数据,并查找特定常量来确定要显示的指示符。

使用以下代码在应用中声明元数据指示符常量:

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;
    

声明这些常量之后,您可以使用它们显示元数据指示符。为了显示在用户浏览媒体浏览树时出现的指示符,可创建一个 extra 包,在其中加入一个或多个常量,然后将该包传递给 MediaDescription.Builder.setExtras() 方法。

以下代码段演示了如何为部分播放的露骨内容媒体项显示指示符:

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 */);
    

如需为当前播放的媒体项显示指示符,您可以在 mediaSessionMediaMetadata.Builder() 方法中声明 EXTRA_IS_EXPLICITEXTRA_IS_DOWNLOADED 的 Long 值。您不能在播放视图上显示 EXTRA_PLAY_COMPLETION_STATE 指示符。

以下代码段演示了如何表明播放视图中的当前歌曲为露骨内容并已下载:

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());
    

为帮助用户浏览您的内容,您的应用可在用户执行语音搜索时,允许用户浏览与其搜索查询相关的一组搜索结果。Android Auto 和 Android Automotive OS 会在界面中以“显示更多结果”条的形式显示这些结果。

图 4. 用于查看相关搜索结果的“更多结果”选项

如需显示可浏览的搜索结果,您应该创建一个常量,并将该常量包含在服务的 onGetRoot() 方法的 extra 包中。

以下代码段演示了如何在 onGetRoot() 方法中启用支持:

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);
    }
    

如需开始提供搜索结果,请在您的媒体浏览器服务中替换 onSearch() 方法。当用户触发“显示更多结果”功能时,Android Auto 和 Android Automotive OS 会将用户的搜索字词转发给此方法。您可以使用标题项整理服务的 onSearch() 方法返回的搜索结果,使结果更易于浏览。例如,如果您的应用是用来播放音乐的,您可以按“Album”、“Artist”和“Songs”整理搜索结果。

以下代码段展示了 onSearch() 方法的简单实现:

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
    }
    

启用播放控制

Android Auto 和 Android Automotive OS 通过服务的 MediaSessionCompat 发送播放控制命令。您必须注册会话并实现关联的回调方法。

注册媒体会话

在媒体浏览器服务的 onCreate() 方法中,创建 MediaSessionCompat,然后通过调用 setSessionToken() 注册媒体会话。

以下代码段演示了如何创建和注册媒体会话:

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());

        ...
    }
    

创建媒体会话对象时,您可以设置用于处理播放控制请求的回调对象。您可以为您的应用提供 MediaSessionCompat.Callback 类的实现来创建此回调对象。下一部分将介绍如何实现此对象。

实现播放命令

当用户从您的应用请求播放媒体项时,Android Automotive OS 和 Android Auto 会使用从应用的媒体浏览器服务获取的 MediaSessionCompat 对象的 MediaSessionCompat.Callback 类。当用户想要控制内容播放时,例如暂停播放或跳至下一曲目,Android Auto 和 Android Automotive OS 会调用其中一个回调对象的方法。

为了处理内容播放,您的应用必须扩展抽象 MediaSessionCompat.Callback 类,并实现应用支持的方法。

您应该实现以下所有回调方法,这些方法适用于应用提供的内容类型:

onPrepare()
在媒体来源改变时调用。Android Automotive OS 也会在启动后立即调用此方法。您的媒体应用必须实现此方法。
onPlay()
如果用户在未选择特定项的情况下选择播放,则调用此方法。您的应用应播放其默认内容。如果之前通过 onPause() 暂停了播放,您的应用应继续播放。

注意:当 Android Automotive OS 或 Android Auto 连接到您的媒体浏览器服务时,您的应用不应自动开始播放音乐。如需了解详情,请参阅设置初始播放状态

onPlayFromMediaId()
在用户选择播放特定项时调用。系统会将您的媒体浏览器服务为内容层次结构中的媒体项分配的 ID 传递给这个方法。
onPlayFromSearch()
在用户选择从搜索查询中播放时调用。应用应根据传入的搜索字符串做出适当的选择。
onPause()
在用户选择暂停播放时调用。
onSkipToNext()
在用户选择跳至下一项时调用。
onSkipToPrevious()
在用户选择跳至上一项时调用。
onStop()
在用户选择停止播放时调用。

您的应用应替换这些方法,以提供任何所需功能。如果应用不支持某一方法,则无需实现该方法。例如,如果您的应用用来直播节目(例如体育广播),则实现 onSkipToNext() 方法毫无意义,但您可以使用 onSkipToNext() 的默认实现。

您的应用无需任何特殊逻辑即可通过汽车扬声器播放内容。当应用收到播放内容的请求时,它应该像平常一样播放音频(例如,通过用户的手机扬声器或耳机播放内容)。Android Auto 和 Android Automotive OS 自动将音频内容发送到车载系统,以通过汽车扬声器播放。

如需详细了解播放音频内容,请参阅媒体播放管理音频播放ExoPlayer

设置标准播放操作

Android Auto 和 Android Automotive OS 根据 PlaybackStateCompat 对象中启用的操作显示播放控件。

默认情况下,您的应用必须支持以下操作:

此外,您还可以创建可向用户显示的播放队列。为此,您需要调用 setQueue()setQueueTitle() 方法,启用 ACTION_SKIP_TO_QUEUE_ITEM 操作,并定义回调 onSkipToQueueItem()

Android Auto 和 Android Automotive OS 显示每个已启用操作的按钮;如果您选择创建播放队列,则也会显示播放队列。

保留未使用的空间

Android Auto 和 Android Automotive OS 会在界面中为 ACTION_SKIP_TO_PREVIOUSACTION_SKIP_TO_NEXT 操作预留空间。此外,Android Auto 也会为播放队列预留空间。如果您的应用不支持其中某一功能,Android Auto 和 Android Automotive OS 会使用对应的空间显示您创建的任何自定义操作。

如果您不希望让自定义操作占据这些空间,则可以保留这些空间。这样,在您的应用不支持相应功能时,Android Auto 和 Android Automotive OS 会使对应空间留空。为此,请创建 extra 包以包含与每个保留功能对应的常量,然后使用这个 extra 包来调用 setExtras() 方法。针对您要保留空间的每个功能,将对应常量设置为 true

以下代码段演示了可用于保留未使用空间的常量:

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 PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT =
            "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"
    private const val PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV =
            "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"
    private const val PLAYBACK_SLOT_RESERVATION_QUEUE =
            "android.media.playback.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 PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT =
        "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
    private static final String PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV =
        "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
    private static final String PLAYBACK_SLOT_RESERVATION_QUEUE =
        "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";
    

设置初始播放状态

当 Android Auto 和 Android Automotive OS 与您的媒体浏览器服务通信时,媒体会话使用 PlaybackState 传达内容播放的状态。当 Android Automotive OS 或 Android Auto 连接到您的媒体浏览器服务时,您的应用不应自动开始播放音乐。而是依靠 Android Auto 和 Android Automotive OS 根据汽车的状态或用户操作恢复或开始播放。

为此,请将媒体会话的初始 PlaybackState 设置为 STATE_STOPPEDSTATE_PAUSEDSTATE_NONESTATE_ERROR

添加自定义播放操作

您可以添加自定义播放操作来显示媒体应用支持的其他操作。如果空间允许(并且未被保留),Android 会将自定义操作添加到传输控件中。否则,自定义操作会显示在溢出菜单中。自定义操作会按添加到 PlaybackState 中的顺序进行显示。

您可以使用 PlaybackStateCompat.Builder 类中的 addCustomAction() 方法添加这些操作。

以下代码段演示了如何添加自定义的“启动电台频道”操作:

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());
    

如需通过更详细的示例来了解此方法,请参阅 GitHub 上 Universal Android Music Player 示例应用中的 setCustomAction() 方法。

创建自定义操作后,媒体会话可以通过替换 onCustomAction() 方法来响应该操作。

以下代码段演示了您的应用如何响应“启动电台频道”操作:

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)) {
            ...
        }
    }
    

如需通过更详细的示例来了解此方法,请参阅 GitHub 上 Universal Android Music Player 示例应用中的 onCustomAction 方法。

自定义操作的图标

您创建的每个自定义操作都需要使用图标资源。汽车中的应用可能会以许多不同的屏幕尺寸和密度来运行,因此您提供的图标必须为矢量可绘制对象。矢量可绘制对象允许缩放资源,同时又不会丢失细节。矢量可绘制对象还能以较小的分辨率轻松地将边和角对准像素边界。

为已停用的操作提供替代图标样式

如果自定义操作不适用于当前上下文,可将自定义操作图标替换为表明操作已被停用的替代图标。

图 5. 样式外自定义操作图标示例。

支持语音操作

您的媒体应用必须支持语音操作,以便为驾驶员提供安全、便利的体验,最大限度地减少干扰。例如,如果您的应用已在播放某一媒体项,则用户可以通过说“播放《波西米亚狂想曲》”让应用播放另一个项,而无需用户查看或轻触汽车的显示屏。

如需通过更详细的示例了解如何在应用中实现语音控制播放操作,请参阅 Google 助理和媒体应用

实现注意力分散预防措施

由于使用 Android Auto 时用户的手机会连接到汽车的扬声器,因此您必须采取额外的预防措施,防止驾驶员分散注意力。

检测车载模式

除非是用户有意识地开始播放(例如,在应用中按播放),否则 Android Auto 媒体应用不得通过汽车扬声器开始播放音频。即使用户在您的媒体应用中设定了闹钟,也不得通过汽车扬声器开始播放音乐。为了满足此要求,应用应在播放任何音频之前确定手机是否处于车载模式。应用可通过调用 getCurrentModeType() 方法检查手机是否处于车载模式。

如果用户的手机处于车载模式,支持闹钟的媒体应用必须执行以下任一操作:

  • 停用闹钟。
  • 通过 STREAM_ALARM 播放闹铃,并在手机屏幕上提供用于停用闹钟的界面。

以下代码段演示了如何检查应用是否在车载模式下运行:

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;
            }
      }
    

处理媒体广告

默认情况下,当媒体元数据在音频播放会话期间发生更改时,Android Auto 会显示通知。当媒体应用从播放音乐切换到播放广告时,向用户显示通知会分散注意力(而且没有这个必要)。在这种情况下,为阻止 Android Auto 显示通知,您必须将媒体元数据键 android.media.metadata.ADVERTISEMENT 设置为 1,如以下代码段所示:

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());
    }
    

处理常规错误

当应用遇到错误时,您应将播放状态设置为 STATE_ERROR,并使用 setErrorMessage() 方法提供错误消息。错误消息必须面向用户,因此必须根据用户当前的语言区域进行本地化。然后,Android Auto 和 Android Automotive OS 就可以向用户显示错误消息了。

如需详细了解错误状态,请参见使用媒体会话:状态和错误

如果 Android Auto 用户需要打开您的手机应用以解决错误,您的消息应该向用户提供该信息。例如,您的错误消息应显示“登录 [应用名称]”,而不是“请登录”。

其他资源