第二个 Android 11 开发者预览版现已推出,快来测试并分享您的反馈吧

为汽车构建 Android 媒体应用

Android Automotive OS 和 Android Auto 可帮助您向用户提供车载媒体应用内容。要简要了解 Android 如何提供车载应用体验,请参阅 Android 汽车应用概览

本指南假定您已具有媒体手机应用。本指南将介绍如何构建适用于 Android Automotive OS 的应用以及如何扩展 Android Auto 手机应用。

准备工作

在开始构建应用之前,请务必遵循 Android 汽车应用使用入门中的步骤,然后再查看本部分中的信息。

关键术语和概念

媒体浏览服务
由您的媒体应用实现且符合 MediaBrowseServiceCompat API 要求的 Android 服务。应用使用此服务将媒体浏览内容公开给 Android Automotive OS 和 Android Auto。
媒体浏览
媒体应用用于将内容公开给 Android Automotive OS 和 Android Auto 的 API。
媒体项
媒体浏览树中的单个 MediaBrowserCompat.MediaItem 对象。媒体项类型包括:
  • 可播放项:这类媒体项表示实际的声音流,如专辑歌曲、图书章节或播客剧集。
  • 可浏览项:这类媒体项将可播放的媒体项整理成组。例如,您可以将多个章节分组为一本图书,将多首歌曲分组为一张专辑,或者将多个剧集分组为一个播客。

注意:既可浏览又可播放的媒体项被视为可播放项。

车辆优化

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

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

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

配置应用的清单文件

您需要配置应用的清单文件,以表明您的应用适用于 Android Automotive OS,并且您的手机应用支持 Android Auto 的媒体服务。

声明对 Android Automotive OS 的支持

您分发到 Android Automotive OS 的应用必须独立于您的手机应用。建议您使用模块Android App Bundle 来帮助您轻松地重复使用代码,以及构建和发布应用。将以下条目添加到 Android Automotive OS 模块的清单文件中,以指示该模块的代码仅限于 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>
    

声明 Android Auto 媒体支持

Android Auto 使用您的手机应用为用户提供面向驾驶员优化的体验。使用以下清单条目来声明您的手机应用支持 Android Auto:

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

此清单条目引用了一个 XML 文件,用于声明您的应用支持的车载功能。要表明您有媒体应用,请将名为 automotive_app_desc.xml 的 XML 文件添加到项目的 res/xml/ 目录中。此文件应包含以下内容:

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

声明媒体浏览服务

Android Automotive OS 和 Android Auto 均可通过媒体浏览服务连接到您的应用,以便浏览媒体项。在清单中声明媒体浏览服务,以便让 Android Automotive OS 和 Android Auto 能够发现服务并连接到您的应用。

以下代码段展示了如何在清单中声明媒体浏览服务。您应将此代码包含在 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 Automotive OS 和 Android Auto 会在不同的位置向用户显示您应用的图标。例如,如果用户正在运行某一导航应用,并且一首歌曲播放结束后会开始一首新歌,则用户会看到包含应用图标的通知。当用户浏览您的媒体内容时,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 Automotive OS 和 Android Auto 可以使用您的服务执行以下操作:

  • 浏览应用的内容层次结构,以向用户展示菜单。
  • 获取应用的 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 提供了一种浏览功能,允许用户从屏幕键盘上选择字母。然后,向用户显示当前抽屉式列表中以该字母开头的项的列表。此功能适用于已排序和未排序的内容,目前仅支持英语。

图 1. 车载屏幕上的字母选择器

图 2. 车载屏幕上的字母顺序列表视图

构建内容层次结构

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

实现 onGetRoot

服务的 onGetRoot() 方法返回有关内容层次结构根节点的信息。Android Automotive OS 和 Android Auto 使用此根节点,通过 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 Automotive OS 和 Android Auto)访问您的内容,当这些系统应用调用 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 Automotive OS 和 Android Auto 会在根节点对象上调用 onLoadChildren() 来获取其子节点,以构建顶级菜单。客户端应用使用子节点对象调用同一方法来构建子菜单。

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

注意:Android Automotive OS 和 Android Auto 对菜单中各层级可显示的媒体项数量有严格的限制。这些限制最大限度地减少对驾驶员的干扰,同时帮助他们通过语音指令操作您的应用。如需了解详情,请参阅浏览内容详情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 extras 包中加入特定常量,来设置媒体项的全局默认显示方式。Android Automotive OS 和 Android Auto 读取与浏览树中各项关联的 extras 包,并查找这些常量来确定相应的样式。

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

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() 方法的 extras 包中,以设置默认内容样式。以下代码段演示了如何将可浏览项的默认内容样式设为网格,并将可播放项的默认内容样式设置为列表:

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 中创建 extras 包。

以下代码段演示了如何创建替换默认内容样式的可浏览 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)
        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);
    }
    

添加标题项

要将媒体项呈现为标题项,您需要使用按项内容样式将这些项分组在一起。组中的每个媒体项都需要在其 MediaDescription 中声明一个使用相同字符串的 extras 包。此字符串将用作组标题,并可以本地化。

Android Automotive OS 和 Android Auto 不会对以这种方式分组的项进行排序。您需要将这些媒体项按照您想要的显示顺序一起传递。

例如,假设您的应用按以下顺序传递了三个媒体项:

  • 媒体项 1,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")
  • 媒体项 2,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Albums")
  • 媒体项 3,extras.putString(EXTRA_CONTENT_STYLE_GROUP_TITLE_HINT, "Songs")

Android Automotive OS 和 Android Auto 不会将媒体项 1 和媒体内容 3 合并到一个名为“Songs”的组中。而是会将这两个媒体项分隔开。

以下代码段演示了如何创建子组标题为 "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")
        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*/);
    }
    

显示其他元数据指示符

图 3. 包含元数据指示符的播放视图

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

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

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;
    

声明这些常量之后,您可以使用它们来显示元数据指示符。要显示在用户浏览媒体浏览树时出现的指示符,可创建一个 extras 包,在其中加入一个或多个常量,然后将该包传递给 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 Automotive OS 和 Android Auto 会在界面中以“显示更多结果”条的形式显示这些结果。

图 4. 车载屏幕上的“显示更多结果”选项

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

以下代码段演示了如何在 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 Automotive OS 和 Android Auto 会将用户的搜索字词转发给此方法。您可以使用标题项来整理服务的 onSearch() 方法返回的搜索结果,使结果更易于浏览。例如,如果您的应用是用来播放音乐的,您可以按“专辑”、“艺人”和“歌曲”来整理搜索结果。

以下代码段演示了 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 Automotive OS 和 Android Auto 通过您的服务的 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 Automotive OS 和 Android Auto 会调用其中一个回调对象的方法。

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

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

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

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

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

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

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

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

设置标准播放操作

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

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

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

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

保留未使用的空间

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

如果您不希望让自定义操作占据这些空间,则可以保留这些空间。这样,在您的应用不支持相应功能时,Android Automotive OS 和 Android Auto 会使对应空间留空。为此,请创建 extras 包以包含与每个保留功能对应的常量,然后使用这个 extras 包来调用 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 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";
    

设置初始播放状态

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

为此,请将媒体会话的初始 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. 样式外自定义操作图标示例。

支持语音操作

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

声明语音操作支持

以下代码段演示了如何在应用的清单文件中声明对语音操作的支持。您应将此代码包含在 Android Automotive OS 模块的清单文件以及手机应用的清单文件中。

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

解析语音搜索查询

当用户搜索特定的媒体项时,例如“在 [应用名称] 中播放爵士乐”或“收听 [歌名]”onPlayFromSearch() 回调方法会接收查询参数和 extras 包中的语音搜索结果。

您的应用可按照以下步骤解析语音搜索查询并开始播放:

  1. 使用从语音搜索返回的 extras 包和搜索查询字符串来过滤结果。
  2. 根据这些结果构建播放队列。
  3. 从结果中播放最相关的媒体项。

onPlayFromSearch() 方法使用 extras 参数以及来自语音搜索的更详细信息。这些 extras 可帮助您在应用中找到要播放的音频内容。如果搜索结果无法提供此数据,您可以实现逻辑来解析原始搜索查询并根据查询播放相应的曲目。

Android Automotive OS 和 Android Auto 支持以下 extras:

以下代码段演示了如何在您的 MediaSession.Callback 实现中替换 onPlayFromSearch() 方法来解析语音搜索结果并始播放:

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

如需通过更详细的示例了解如何在应用中实现语音搜索来播放音频内容,请参见 Universal Media Player 示例。

处理空查询

当用户说“在 [应用名称] 中播放音乐”时,Android Automotive OS 或 Android Auto 会尝试通过调用应用的 onPlayFromSearch() 方法启动您的应用并播放音频。但是,由于用户没有说出媒体项的名称,onPlayFromSearch() 方法会收到空的查询参数。在这些情况下,您的应用应通过立即播放音频做出响应,例如,播放最近播放列表或随机队列中的歌曲。

实现语音控制播放操作

要在用户驾车和聆听媒体内容时提供免触摸体验,您的应用必须允许用户通过语音操作来控制内容播放。当用户说出诸如“下一首歌”、“暂停播放音乐”或“继续播放音乐”等命令时,系统会触发您在其中实现了播放控制操作的相应回调方法。

要提供语音控制播放操作,请首先在应用的 MediaSession 对象中设置以下标志以启用硬件控制:

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

设置好这些标志后,使用应用中支持的播放控件来实现回调方法。Android Automotive OS 和 Android Auto 支持以下语音控制播放操作:

示例短语 回调方法
“下一首歌” onSkipToNext()
“上一首歌” onSkipToPrevious()
“暂停播放音乐” onPause()
“停止播放音乐” onStop()
“继续播放音乐” onPlay()

如需通过更详细的示例了解如何在应用中实现语音控制播放操作,请参见 Universal Media Player 示例。

实现 Android Automotive OS 的设置和登录 Activity

除了媒体浏览服务外,您还可以为 Android Automotive OS 应用提供车辆优化设置和登录 Activity。通过这些 Activity,您可以提供 Android Media API 中未包含的应用功能。

添加“设置”Activity

您可以添加车辆优化的“设置”Activity,以便用户可以在汽车中配置应用设置。您的“设置”Activity 还可以提供其他工作流,例如登录或退出用户帐号或者切换用户帐号。

“设置”Activity 工作流

“设置”Activity 可为用户提供不同的工作流。下图显示了用户如何使用 Android Automotive OS 与“设置”Activity 进行互动:

“设置”Activity 的工作流

图 6. “设置”Activity 的工作流示意图

声明“设置”Activity

您必须在应用的清单文件中声明您的“设置”Activity,如以下代码段所示:

<application>
        ...
        <activity android:name=".AppSettingsActivity"
                  android:exported="true"
                  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>
    

实现“设置”Activity

当用户启动您的应用时,Android Automotive OS 会检测您声明的“设置”Activity 并显示对应功能。用户可以使用汽车的显示屏来点按或选择此功能,以导航至对应 Activity。Android Automotive OS 会发送 ACTION_APPLICATION_PREFERENCES intent,告知您的应用启动“设置”Activity。

添加“登录”Activity

如果您的应用需要用户登录后才能使用,您可以添加车辆优化的“登录”Activity 来处理应用的登录和退出操作。您也可以在“设置”Activity 中添加登录和退出工作流,但如果用户只有登录后才能使用应用,则应使用专门的“登录”Activity。

“登录”Activity 工作流

下图显示了用户如何使用 Android Automotive OS 与“登录”Activity 进行互动:

“登录”Activity 的工作流

图 7. “登录”Activity 的工作流示意图

应用启动时要求登录

若要使用“登录”Activity 要求用户登录后才能使用应用,则媒体浏览服务必须执行以下操作:

  1. 使用 setState() 方法将媒体会话的 PlaybackState 设为 STATE_ERROR。这会告知 Android Automotive OS,只有错误得到解决后才能执行其他操作。
  2. 将媒体会话的 PlaybackState 错误代码设置为 ERROR_CODE_AUTHENTICATION_EXPIRED。这会告知 Android Automotive OS,用户需要进行身份验证。
  3. 使用 setErrorMessage() 方法设置媒体会话的 PlaybackState 错误消息。由于此错误消息是面向用户的,因此必须根据用户当前的语言区域对消息进行本地化。
  4. 使用 setExtras() 方法设置媒体会话的 PlaybackState extras。添加以下两个键:

    • android.media.extras.ERROR_RESOLUTION_ACTION_LABEL:开始登录工作流的按钮上所显示的字符串。由于此字符串是面向用户的,因此必须针对用户当前的语言区域进行本地化。
    • android.media.extras.ERROR_RESOLUTION_ACTION_INTENT:一个 PendingIntent,在用户点按通过 android.media.extras.ERROR_RESOLUTION_ACTION_LABEL 指代的按钮时,它将用户定向到您的“登录”Activity。

以下代码段演示了您的应用如何要求用户登录后才能使用应用:

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

用户成功通过身份验证后,您的应用必须将 PlaybackState 重新设置为 STATE_ERROR 以外的状态,然后通过调用 Activity 的 finish() 方法使用户返回到 Android Automotive OS。

实现“登录”Activity

Google 提供了多种身份验证工具,供您用于帮助用户在汽车内登录您的应用。Firebase Authentication 等一些工具提供全栈工具包,可帮助您构建自定义身份验证体验。其他工具利用用户现有的凭据或其他技术帮助您为用户打造无缝的登录体验。

建议您使用以下工具,为之前在其他设备上登录过的用户提供更为简便的登录体验:

  • Google 登录:如果您已面向其他设备(如手机应用)实现 Google 登录,您也应该为 Android Automotive OS 应用实现 Google 登录,以便为现有的 Google 登录用户提供支持。
  • Google 自动填充:如果用户在其他 Android 设备上选择使用 Google 自动填充,其凭据会保存到 Google 密码管理器中。以后,当用户登录您的 Android Automotive OS 应用时,Google 自动填充会建议相关的已保存凭据。使用 Google 自动填充不需要开发应用;但是,应用开发者应优化其应用以提高质量。运行 Android Oreo 8.0(API 级别 26)或更高版本(包括 Android Automotive OS)的所有设备均支持 Google 自动填充。

处理登录保护式操作

某些应用允许用户匿名访问某些操作,但要求用户先登录后才能执行其他操作。例如,用户无需登录即可在应用中播放音乐,但必须登录后才能跳过歌曲。

在这种情况下,当用户尝试执行受限操作(跳过歌曲)时,您的应用可以通过发出非严重错误来建议用户进行身份验证。通过使用非严重错误,系统会向用户显示消息,而不会中断当前媒体项的播放。若要实现非严重错误处理,请完成以下步骤:

  1. 将媒体会话的 PlaybackStateerrorCode 设置为 ERROR_CODE_AUTHENTICATION_EXPIRED。这会告知 Android Automotive OS,用户需要进行身份验证。
  2. 使媒体会话的 PlaybackStatestate 保持不变,不要将它设置为 STATE_ERROR。这会告知系统,错误不是严重错误。
  3. 使用 setExtras() 方法设置媒体会话的 PlaybackState extras。添加以下两个键:

    • android.media.extras.ERROR_RESOLUTION_ACTION_LABEL:开始登录工作流的按钮上所显示的字符串。由于此字符串是面向用户的,因此必须针对用户当前的语言区域进行本地化。
    • android.media.extras.ERROR_RESOLUTION_ACTION_INTENT:一个 PendingIntent,在用户点按通过 android.media.extras.ERROR_RESOLUTION_ACTION_LABEL 指代的按钮时,它将用户定向到您的“登录”Activity。
  4. 使媒体会话的其余 PlaybackState 状态保持不变。这允许在用户决定是否登录时继续播放当前媒体项。

实现注意力分散预防措施

由于使用 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 Automotive OS 和 Android Auto 就可以向用户显示错误消息了。

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

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

其他资源