Android Auto 和 Android Automotive OS 可帮助您将媒体应用内容呈现给汽车中的用户。
构建车载媒体应用有两种方式
本指南介绍了如何使用
MediaBrowserService
和MediaSession
来创建应用,Android Auto 和 Android Automotive OS 可以连接到该应用,以呈现针对车载使用优化的媒体浏览和播放视图。媒体应用还可以使用 Car App Library 模板构建,以实现可自定义的格式设置、浏览功能和扩展自定义操作。有关实现详情,请参阅构建模板化媒体应用。模板化媒体应用目前仅支持 Android Auto。
本指南介绍了您的应用在 Android Auto 或 Android Automotive OS 上运行所需的 MediaBrowserService
和 MediaSession
的必需组件。完成核心媒体基础架构后,您可以为您的媒体应用添加 Android Auto 支持和添加 Android Automotive OS 支持。
本指南假设您已有一个可在手机上播放音频的媒体应用,并且您的媒体应用符合 Android 媒体应用架构。
开始之前
- 查看 Android 媒体 API 文档。
- 查看 创建媒体应用 以获取设计指导。
- 查看本节中列出的关键术语和概念。
关键术语和概念
- 媒体浏览器服务
- 由您的媒体应用实现的 Android 服务,符合
MediaBrowserServiceCompat
API。您的应用使用此服务来公开其内容。 - 媒体浏览器
- 媒体应用用于发现媒体浏览器服务并显示其内容的 API。Android Auto 和 Android Automotive OS 使用媒体浏览器查找您的应用的媒体浏览器服务。
- 媒体项
媒体浏览器将其内容组织成一个
MediaItem
对象树。媒体项可以具有以下一个或两个标志FLAG_PLAYABLE
:表示该项是内容树上的叶节点。该项表示单个声音流,例如专辑中的歌曲、有声读物中的章节或播客的一集。FLAG_BROWSABLE
:表示该项是内容树上的一个节点,并且它有子节点。例如,该项表示一个专辑,其子项是专辑中的歌曲。
既可浏览又可播放的媒体项就像一个播放列表。您可以选择该项本身来播放其所有子项,也可以浏览其子项。
- 车载优化
Android Automotive OS 应用的一项 Activity,它遵循 Android Automotive OS 设计准则。这些 Activity 的界面不是由 Android Automotive OS 绘制的,因此您必须确保您的应用遵循设计准则。通常,这包括更大的点击目标和字体大小、支持白天和夜晚模式以及更高的对比度。
仅当车载用户体验限制 (CUXRs) 未生效时,才允许显示车载优化用户界面,因为这些界面可能需要用户长时间的注意力或交互。CUXRs 在汽车停止或停放时未生效,但在汽车行驶时始终生效。
您无需为 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 可用于在系统界面中表示您的应用的图标。需要两种图标类型
- 启动器图标
- 归因图标
启动器图标
启动器图标表示您的应用在系统界面中的显示,例如在启动器和图标托盘中。您可以使用以下清单声明指定要使用您的移动应用图标来表示您的车载媒体应用
<application
...
android:icon="@mipmap/ic_launcher"
...
/>
要使用与您的移动应用图标不同的图标,请在清单中您的媒体浏览器服务的 <service>
元素上设置 android:icon
属性
<application>
...
<service
...
android:icon="@mipmap/auto_launcher"
...
/>
</application>
归因图标

图 1. 媒体卡上的归因图标。
归因图标用于媒体内容优先的场所,例如媒体卡上。考虑重复使用用于通知的小图标。此图标必须是单色的。您可以使用以下清单声明指定用于表示您的应用的图标
<application>
...
<meta-data
android:name="androidx.car.app.TintableAttributionIcon"
android:resource="@drawable/ic_status_icon" />
...
</application>
创建您的媒体浏览器服务
您可以通过扩展 MediaBrowserServiceCompat
类来创建媒体浏览器服务。然后,Android Auto 和 Android Automotive OS 可以使用您的服务执行以下操作
- 浏览您的应用的内容层次结构,向用户呈现菜单。
- 获取您的应用的
MediaSessionCompat
对象的令牌,以控制音频播放。
您还可以使用媒体浏览器服务,让其他客户端访问您的应用中的媒体内容。这些媒体客户端可以是用户手机上的其他应用,也可以是其他远程客户端。
媒体浏览器服务工作流
本节描述了 Android Automotive OS 和 Android Auto 在典型用户工作流期间如何与您的媒体浏览器服务进行交互。
- 用户在 Android Automotive OS 或 Android Auto 上启动您的应用。
- Android Automotive OS 或 Android Auto 使用
onCreate()
方法联系您的应用的媒体浏览器服务。在onCreate()
方法的实现中,您必须创建并注册一个MediaSessionCompat
对象及其回调对象。 - Android Automotive OS 或 Android Auto 调用您的服务的
onGetRoot()
方法来获取内容层次结构中的根媒体项。根媒体项不显示;相反,它用于从您的应用中检索更多内容。 - Android Automotive OS 或 Android Auto 调用您的服务的
onLoadChildren()
方法来获取根媒体项的子项。Android Automotive OS 和 Android Auto 将这些媒体项显示为内容项的顶层。有关系统在此级别上期望的内容的更多信息,请参阅本页上的构建根菜单结构。 - 如果用户选择一个可浏览的媒体项,您的服务的
onLoadChildren()
方法将再次被调用,以检索所选菜单项的子项。 - 如果您的应用支持,当用户选择一个可播放的媒体项时,Android Automotive OS 或 Android Auto 会调用相应的媒体会话回调方法来执行该操作。
- 如果您的应用支持,用户还可以搜索您的内容。在这种情况下,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 上的通用 Android 音乐播放器示例应用中的 onGetRoot()
方法。
为 onGetRoot() 添加软件包验证
当调用您的服务的 onGetRoot()
方法时,调用软件包会将识别信息传递给您的服务。您的服务可以使用此信息来决定该软件包是否可以访问您的内容。例如,您可以通过将 clientPackageName
与您的允许列表进行比较并验证用于签署软件包 APK 的证书来限制对您的应用内容的访问,使其仅限于批准的软件包列表。如果无法验证该软件包,则返回 null
以拒绝访问您的内容。
为了向 Android Auto 和 Android Automotive OS 等系统应用提供对您的内容的访问权限,当这些系统应用调用 onGetRoot()
方法时,您的服务必须始终返回非空的 BrowserRoot
。Android Automotive OS 系统应用的签名可能因汽车的品牌和型号而异,因此您需要允许所有系统应用的连接,以稳健地支持 Android Automotive OS。
以下代码片段展示了您的服务如何验证调用软件包是否为系统应用
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 上的通用 Android 音乐播放器示例应用中的 PackageValidator
类。有关如何为您的服务的 onGetRoot()
方法实现软件包验证的更详细示例,请参阅该类。
除了允许系统应用之外,您还必须允许 Google Assistant 连接到您的 MediaBrowserService
。请注意,Google Assistant 为手机(包括 Android Auto)和 Android Automotive OS 都具有单独的软件包名称。
实现 onLoadChildren()
收到您的根节点对象后,Android Auto 和 Android Automotive OS 通过在根节点对象上调用 onLoadChildren()
来获取其子项,从而构建顶层菜单。客户端应用通过使用子节点对象调用此相同方法来构建子菜单。
内容层次结构中的每个节点都由一个 MediaBrowserCompat.MediaItem
对象表示。每个媒体项都由唯一的 ID 字符串标识。客户端应用将这些 ID 字符串视为不透明的令牌。当客户端应用想要浏览到子菜单或播放媒体项时,它会传递该令牌。您的应用负责将该令牌与相应的媒体项关联。
以下代码片段展示了 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 whether 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 whether 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 上的通用 Android 音乐播放器示例应用中的 onLoadChildren()
方法。
构建根菜单结构

图 2. 作为导航标签显示的根内容。
Android Auto 和 Android Automotive OS 对根菜单的结构有特定限制。这些限制通过根提示传达给 MediaBrowserService
,可以通过传递给 onGetRoot()
的 Bundle
参数读取。遵循这些提示可让系统将根内容最佳地显示为导航标签。如果您不遵循这些提示,某些根内容可能会被系统丢弃或变得不易发现。 发送了两个提示
- 根子项数量限制:在大多数情况下,您可以预期此数字为四。这意味着不能显示超过四个标签页。
- 根子项支持的标志:您可以预期此值为
MediaItem#FLAG_BROWSABLE
。这意味着只有可浏览的项(而非可播放的项)可以显示为标签页。
使用以下代码读取相关的根提示
Kotlin
import androidx.media.utils.MediaConstants // Later, in your MediaBrowserServiceCompat. override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle ): BrowserRoot { val maximumRootChildLimit = rootHints.getInt( MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, /* defaultValue= */ 4) val supportedRootChildFlags = rootHints.getInt( MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, /* defaultValue= */ MediaItem.FLAG_BROWSABLE) // Rest of method... }
Java
import androidx.media.utils.MediaConstants; // Later, in your MediaBrowserServiceCompat. @Override public BrowserRoot onGetRoot( String clientPackageName, int clientUid, Bundle rootHints) { int maximumRootChildLimit = rootHints.getInt( MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT, /* defaultValue= */ 4); int supportedRootChildFlags = rootHints.getInt( MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS, /* defaultValue= */ MediaItem.FLAG_BROWSABLE); // Rest of method... }
您可以根据这些提示的值选择分支内容层次结构的逻辑,特别是当您的层次结构在 Android Auto 和 Android Automotive OS 之外的 MediaBrowser
集成之间有所不同时。例如,如果您通常显示一个根可播放项,由于受支持标志提示的值,您可能希望将其嵌套在根可浏览项下。
除了根提示之外,还有一些额外的准则需要遵循,以帮助确保标签页最佳呈现
- 为每个标签项提供单色图标,最好是白色。
- 为每个标签项提供简短但有意义的标签。保持标签简短可以减少字符串被截断的机会。
显示媒体封面
媒体项的封面必须使用 ContentResolver.SCHEME_CONTENT
或 ContentResolver.SCHEME_ANDROID_RESOURCE
作为本地 URI 传递。此本地 URI 必须解析为应用资源中的位图或矢量 drawable。对于表示内容层次结构中项的 MediaDescriptionCompat
对象,通过 setIconUri()
传递 URI。对于表示当前播放项的 MediaMetadataCompat
对象,通过 putString()
传递 URI,使用以下任何键
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI
MediaMetadataCompat.METADATA_KEY_ART_URI
MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI
以下步骤描述了如何从 Web URI 下载封面并通过本地 URI 公开它。有关更完整的示例,请参阅通用 Android 音乐播放器示例应用中 openFile()
及其周围方法的实现。
构建与 Web URI 对应的
content://
URI。媒体浏览器服务和媒体会话会将此内容 URI 传递给 Android Auto 和 Android Automotive OS。Kotlin
fun Uri.asAlbumArtContentURI(): Uri { return Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(CONTENT_PROVIDER_AUTHORITY) .appendPath(this.getPath()) // Make sure you trust the URI .build() }
Java
public static Uri asAlbumArtContentURI(Uri webUri) { return new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(CONTENT_PROVIDER_AUTHORITY) .appendPath(webUri.getPath()) // Make sure you trust the URI! .build(); }
在您的
ContentProvider.openFile()
实现中,检查相应 URI 的文件是否存在。如果不存在,则下载并缓存图像文件。以下代码片段使用 Glide。Kotlin
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { val context = this.context ?: return null val file = File(context.cacheDir, uri.path) if (!file.exists()) { val remoteUri = Uri.Builder() .scheme("https") .authority("my-image-site") .appendPath(uri.path) .build() val cacheFile = Glide.with(context) .asFile() .load(remoteUri) .submit() .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS) cacheFile.renameTo(file) file = cacheFile } return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) }
Java
@Nullable @Override public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { Context context = this.getContext(); File file = new File(context.getCacheDir(), uri.getPath()); if (!file.exists()) { Uri remoteUri = new Uri.Builder() .scheme("https") .authority("my-image-site") .appendPath(uri.getPath()) .build(); File cacheFile = Glide.with(context) .asFile() .load(remoteUri) .submit() .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS); cacheFile.renameTo(file); file = cacheFile; } return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); }
有关内容提供者的更多详细信息,请参阅 创建内容提供者。
应用内容样式
使用可浏览或可播放项构建内容层次结构后,您可以应用内容样式来确定这些项在汽车中的显示方式。
您可以使用以下内容样式
- 列表项
-
此内容样式优先显示标题和元数据,而非图像。
- 网格项
-
此内容样式优先显示图像,而非标题和元数据。
设置默认内容样式
您可以通过在服务的 onGetRoot()
方法的 BrowserRoot
extra 捆绑包中包含某些常量来设置媒体项的全局显示默认值。Android Auto 和 Android Automotive OS 会读取此捆绑包并查找这些常量以确定适当的样式。
以下 extra 可用作捆绑包中的键
DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE
:表示浏览树中所有可浏览项的呈现提示。DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE
:表示浏览树中所有可播放项的呈现提示。
这些键可以映射到以下整数常量值,以影响这些项的呈现
DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
:相应项作为列表项呈现。DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
:相应项作为网格项呈现。DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM
:相应项作为“类别”列表项呈现。它们与普通列表项相同,只是在项的图标周围应用了边距,因为图标在较小时看起来更好。图标必须是可着色的矢量 drawable。此提示预计仅提供给可浏览项。DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM
:相应项作为“类别”网格项呈现。它们与普通网格项相同,只是在项的图标周围应用了边距,因为图标在较小时看起来更好。图标必须是可着色的矢量 drawable。此提示预计仅提供给可浏览项。
以下代码片段展示了如何将可浏览项的默认内容样式设置为网格,将可播放项的默认内容样式设置为列表
Kotlin
import androidx.media.utils.MediaConstants @Nullable override fun onGetRoot( @NonNull clientPackageName: String, clientUid: Int, @Nullable rootHints: Bundle ): BrowserRoot { val extras = Bundle() extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM) extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM) return BrowserRoot(ROOT_ID, extras) }
Java
import androidx.media.utils.MediaConstants; @Nullable @Override public BrowserRoot onGetRoot( @NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) { Bundle extras = new Bundle(); extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM); extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM); return new BrowserRoot(ROOT_ID, extras); }
设置每项内容样式
内容样式 API 允许您覆盖任何可浏览媒体项的子项以及任何媒体项本身的默认内容样式。
要覆盖可浏览媒体项的子项的默认设置,请在媒体项的 MediaDescription
中创建一个 extras 捆绑包,并添加前面提到的相同提示。DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE
适用于该项的可播放子项,而 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE
适用于该项的可浏览子项。
要覆盖特定媒体项本身(而非其子项)的默认设置,请在媒体项的 MediaDescription
中创建一个 extras 捆绑包,并添加一个键为 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM
的提示。使用前面描述的相同值来指定该项的呈现方式。
以下代码片段展示了如何创建一个可浏览的 MediaItem
,它会覆盖其自身及其子项的默认内容样式。它将自身样式化为类别列表项,其可浏览子项样式化为列表项,其可播放子项样式化为网格项
Kotlin
import androidx.media.utils.MediaConstants 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( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM) extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM) extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM) mediaDescriptionBuilder.setExtras(extras) return MediaBrowser.MediaItem( mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE) }
Java
import androidx.media.utils.MediaConstants; 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( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM); extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM); extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM); mediaDescriptionBuilder.setExtras(extras); return new MediaBrowser.MediaItem( mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE); }
使用标题提示对项进行分组
要将相关的媒体项分组在一起,您可以使用每项提示。组中的每个媒体项都需要在它们的 MediaDescription
中声明一个 extras 捆绑包,该捆绑包包含一个映射,其键为 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE
,并具有相同的字符串值。将此字符串本地化,该字符串用作组的标题。
以下代码片段展示了如何创建一个带有子组标题 "Songs"
的 MediaItem
Kotlin
import androidx.media.utils.MediaConstants 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( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs") mediaDescriptionBuilder.setExtras(extras) return MediaBrowser.MediaItem( mediaDescriptionBuilder.build(), /* playable or browsable flag*/) }
Java
import androidx.media.utils.MediaConstants; 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( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs"); mediaDescriptionBuilder.setExtras(extras); return new MediaBrowser.MediaItem( mediaDescriptionBuilder.build(), /* playable or browsable flag*/); }
您的应用必须将所有您想要分组的媒体项作为连续块传递。例如,假设您想要按顺序显示“歌曲”和“专辑”两个媒体项组,并且您的应用按以下顺序传递五个媒体项
- 媒体项 A,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
- 媒体项 B,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
- 媒体项 C,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
- 媒体项 D,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
- 媒体项 E,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
因为“歌曲”组和“专辑”组的媒体项没有保持在连续的块中,Android Auto 和 Android Automotive OS 将其解释为以下四个组
- 组 1,名为“歌曲”,包含媒体项 A
- 组 2,名为“专辑”,包含媒体项 B
- 组 3,名为“歌曲”,包含媒体项 C 和 D
- 组 4,名为“专辑”,包含媒体项 E
要将这些项显示在两个组中,您的应用必须改为按以下顺序传递媒体项
- 媒体项 A,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
- 媒体项 C,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
- 媒体项 D,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
- 媒体项 B,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
- 媒体项 E,带有
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
显示额外的元数据指示器
您可以包含额外的元数据指示器,以便在媒体浏览器树中和播放期间提供内容的即时信息。在浏览树中,Android Auto 和 Android Automotive OS 会读取与项关联的 extras 并查找某些常量来确定要显示的指示器。在媒体播放期间,Android Auto 和 Android Automotive OS 会读取媒体会话的元数据并查找某些常量来确定要显示的指示器。

图 3. 播放视图,带有识别歌曲和艺术家的元数据,以及一个表示明确内容的图标。

图 4. 浏览视图,第一个项上有一个未播放内容的圆点,第二个项上有一个部分播放内容的进度条。
以下常量可用于 MediaItem
描述 extras 和 MediaMetadata
extras 两者
EXTRA_DOWNLOAD_STATUS
:指示项的下载状态。使用此常量作为键;以下 long 型常量是可能的值STATUS_DOWNLOADED
:该项已完全下载。STATUS_DOWNLOADING
:该项正在下载中。STATUS_NOT_DOWNLOADED
:该项未下载。
METADATA_KEY_IS_EXPLICIT
:指示该项是否包含露骨内容。要指示某项是露骨内容,请使用此常量作为键,并使用 long 型METADATA_VALUE_ATTRIBUTE_PRESENT
作为值。
以下常量仅可用于 MediaItem
描述 extras
DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS
:指示长格式内容(如播客剧集或有声读物)的完成状态。使用此常量作为键;以下整数常量是可能的值DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
:该项根本没有播放过。DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
:该项已部分播放,当前位置位于中间某个地方。DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
:该项已完成。
DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE
:指示长格式内容的完成进度,以 0.0 到 1.0(含)之间的双精度浮点数表示。此 extra 提供了有关PARTIALLY_PLAYING
状态的更多信息,以便 Android Auto 或 Android Automotive OS 显示更有意义的进度指示器,例如进度条。如果您使用此 extra,请参阅本指南中关于在播放内容时更新浏览视图中的进度条的部分,了解如何在初始显示后保持此指示器最新。
要显示用户浏览媒体浏览树时出现的指示器,请创建一个包含这些常量中的一个或多个的 extras 捆绑包,并将该捆绑包传递给 MediaDescription.Builder.setExtras()
方法。
以下代码片段展示了如何显示已完成 70% 的明确媒体项的指示器
Kotlin
import androidx.media.utils.MediaConstants val extras = Bundle() extras.putLong( MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT) extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED) extras.putDouble( MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.7) val description = MediaDescriptionCompat.Builder() .setMediaId(/*...*/) .setTitle(resources.getString(/*...*/)) .setExtras(extras) .build() return MediaBrowserCompat.MediaItem(description, /* flags */)
Java
import androidx.media.utils.MediaConstants; Bundle extras = new Bundle(); extras.putLong( MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT); extras.putInt( MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED); extras.putDouble( MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.7); MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() .setMediaId(/*...*/) .setTitle(resources.getString(/*...*/)) .setExtras(extras) .build(); return new MediaBrowserCompat.MediaItem(description, /* flags */);
要显示当前正在播放的媒体项的指示器,您可以在 mediaSession
的 MediaMetadataCompat
中为 METADATA_KEY_IS_EXPLICIT
或 EXTRA_DOWNLOAD_STATUS
声明 Long
值。您无法在播放视图上显示 DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS
或 DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE
指示器。
以下代码片段展示了如何指示播放视图中当前歌曲是露骨的且已下载
Kotlin
import androidx.media.utils.MediaConstants mediaSession.setMetadata( MediaMetadataCompat.Builder() .putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Song Name") .putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name") .putString( MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, albumArtUri.toString()) .putLong( MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT) .putLong( MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS, MediaDescriptionCompat.STATUS_DOWNLOADED) .build())
Java
import androidx.media.utils.MediaConstants; mediaSession.setMetadata( new MediaMetadataCompat.Builder() .putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Song Name") .putString( MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name") .putString( MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, albumArtUri.toString()) .putLong( MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT) .putLong( MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS, MediaDescriptionCompat.STATUS_DOWNLOADED) .build());
在内容播放时更新浏览视图中的进度条
如前所述,您可以使用 DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE
extra 在浏览视图中为部分播放的内容显示进度条。但是,如果用户从 Android Auto 或 Android Automotive OS 继续播放部分播放的内容,该指示器会随着时间的推移变得不准确。
为了让 Android Auto 和 Android Automotive OS 保持进度条最新,您可以在 MediaMetadataCompat
和 PlaybackStateCompat
中提供额外信息,将正在进行的内容链接到浏览视图中的媒体项。媒体项必须满足以下要求才能拥有自动更新的进度条
- 创建时,
MediaItem
必须在其 extras 中发送DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE
,其值在 0.0 到 1.0(含)之间。 MediaMetadataCompat
必须发送METADATA_KEY_MEDIA_ID
,其字符串值等于传递给MediaItem
的媒体 ID。PlaybackStateCompat
必须包含一个 extra,其键为PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID
,映射到一个字符串值,该值等于传递给MediaItem
的媒体 ID。
以下代码片段展示了如何指示当前播放的项已链接到浏览视图中的项
Kotlin
import androidx.media.utils.MediaConstants // When the MediaItem is constructed to show in the browse view. // Suppose the item was 25% complete when the user launched the browse view. val mediaItemExtras = Bundle() mediaItemExtras.putDouble( MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25) val description = MediaDescriptionCompat.Builder() .setMediaId("my-media-id") .setExtras(mediaItemExtras) // ...and any other setters. .build() return MediaBrowserCompat.MediaItem(description, /* flags */) // Elsewhere, when the user has selected MediaItem for playback. mediaSession.setMetadata( MediaMetadataCompat.Builder() .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id") // ...and any other setters. .build()) val playbackStateExtras = Bundle() playbackStateExtras.putString( MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id") mediaSession.setPlaybackState( PlaybackStateCompat.Builder() .setExtras(playbackStateExtras) // ...and any other setters. .build())
Java
import androidx.media.utils.MediaConstants; // When the MediaItem is constructed to show in the browse view. // Suppose the item was 25% complete when the user launched the browse view. Bundle mediaItemExtras = new Bundle(); mediaItemExtras.putDouble( MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25); MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() .setMediaId("my-media-id") .setExtras(mediaItemExtras) // ...and any other setters. .build(); return MediaBrowserCompat.MediaItem(description, /* flags */); // Elsewhere, when the user has selected MediaItem for playback. mediaSession.setMetadata( new MediaMetadataCompat.Builder() .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id") // ...and any other setters. .build()); Bundle playbackStateExtras = new Bundle(); playbackStateExtras.putString( MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id"); mediaSession.setPlaybackState( new PlaybackStateCompat.Builder() .setExtras(playbackStateExtras) // ...and any other setters. .build());
显示可浏览的搜索结果

图 5. 播放视图,带有“搜索结果”选项,用于查看与用户语音搜索相关的媒体项。
您的应用可以提供上下文搜索结果,这些结果在用户发起搜索查询时显示给用户。Android Auto 和 Android Automotive OS 通过搜索查询界面或通过在会话早期进行的查询上进行枢轴的界面功能来显示这些结果。要了解更多信息,请参阅本指南中的支持语音操作部分。
要显示可浏览的搜索结果,请在您的服务的 onGetRoot()
方法的 extras 捆绑包中包含常量键 BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED
,并将其映射到布尔值 true
。
以下代码片段展示了如何在 onGetRoot()
方法中启用支持
Kotlin
import androidx.media.utils.MediaConstants @Nullable fun onGetRoot( @NonNull clientPackageName: String, clientUid: Int, @Nullable rootHints: Bundle ): BrowserRoot { val extras = Bundle() extras.putBoolean( MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true) return BrowserRoot(ROOT_ID, extras) }
Java
import androidx.media.utils.MediaConstants; @Nullable @Override public BrowserRoot onGetRoot( @NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) { Bundle extras = new Bundle(); extras.putBoolean( MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true); return new BrowserRoot(ROOT_ID, extras); }
要开始提供搜索结果,请在您的媒体浏览器服务中覆盖 onSearch()
方法。每当用户调用搜索查询界面或“搜索结果”界面功能时,Android Auto 和 Android Automotive OS 都会将用户的搜索词转发到此方法。
您可以使用标题项来组织您的服务的 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<List<MediaItem>> result) { // Detach from results to unblock the caller (if a search is expensive). result.detach(); new AsyncTask<Void, Void, Void>() { List<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); } } }.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. }
自定义浏览操作

图 6. 单个自定义浏览操作
自定义浏览操作允许您在车载媒体应用中为您的应用的 MediaItem
对象添加自定义图标和标签,并处理用户与这些操作的交互。这使您可以通过多种方式扩展媒体应用的功能,例如添加“下载”、“添加到队列”、“播放广播”、“收藏”或“删除”操作。

图 7. 自定义浏览操作溢出菜单
如果自定义操作的数量超过原始设备制造商允许显示的数量,将向用户呈现溢出菜单。
它们如何工作?
每个自定义浏览操作都定义为
- 操作 ID(唯一的字符串标识符)
- 操作标签(显示给用户的文本)
- 操作图标 URI(可着色的矢量 drawable)
您将自定义浏览操作列表全局定义为 BrowseRoot
的一部分。然后您可以将这些操作的子集附加到单个 MediaItem.
当用户与自定义浏览操作交互时,您的应用会在 onCustomAction()
中收到回调。然后您可以处理该操作,并在必要时更新 MediaItem
的操作列表。这对于“收藏”和“下载”等有状态操作非常有用。对于不需要更新的操作,如“播放广播”,您无需更新操作列表。

图 8. 自定义浏览操作工具栏
您还可以将自定义浏览操作附加到浏览节点根。这些操作将显示在主工具栏下方的辅助工具栏中。
如何实现自定义浏览操作
以下是将自定义浏览操作添加到您的项目的步骤
- 在您的
MediaBrowserServiceCompat
实现中覆盖两个方法 - 在运行时解析操作限制
- 在
onGetRoot()
中,使用rootHints
Bundle
中的键BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT
获取每个MediaItem
允许的最大操作数量。限制为 0 表示系统不支持该功能。
- 在
- 构建自定义浏览操作的全局列表
- 对于每个操作,创建一个包含以下键的
Bundle
对象:*EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID
:操作 ID *EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL
:操作标签 *EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI
:操作图标 URI * 将所有操作Bundle
对象添加到列表中。
- 对于每个操作,创建一个包含以下键的
- 将全局列表添加到您的
BrowseRoot
- 在
BrowseRoot
extrasBundle
中,使用键BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
将操作列表作为Parcelable
Arraylist
添加。
- 在
- 将操作添加到您的
MediaItem
对象- 您可以通过在
MediaDescriptionCompat
extras 中使用键DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
包含操作 ID 列表,从而将操作添加到单个MediaItem
对象。此列表必须是您在BrowseRoot
中定义的全局操作列表的子集。
- 您可以通过在
- 处理操作并返回进度或结果
- 在
onCustomAction
中,根据操作 ID 和您需要的任何其他数据处理操作。您可以使用键EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID
从 extras 中获取触发该操作的MediaItem
的 ID。 - 您可以通过在进度或结果捆绑包中包含键
EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
来更新MediaItem
的操作列表。
- 在
以下是您可以在 BrowserServiceCompat
中进行的一些更改,以便开始使用自定义浏览操作。
覆盖 BrowserServiceCompat
您需要在 MediaBrowserServiceCompat
中覆盖以下方法。
public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result)
public void onCustomAction(@NonNull String action, Bundle extras, @NonNull Result<Bundle> result)
解析操作限制
您应该检查支持多少个自定义浏览操作。
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { rootHints.getInt( MediaConstants.BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT, 0) }
构建自定义浏览操作
每个操作都需要打包到一个单独的 Bundle
中。
- 操作 ID
bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID, "<ACTION_ID>")
- 操作标签
bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL, "<ACTION_LABEL>")
- 操作图标 URI
bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI, "<ACTION_ICON_URI>")
将自定义浏览操作添加到 Parceable
ArrayList
将所有自定义浏览操作 Bundle
对象添加到 ArrayList
中。
private ArrayList<Bundle> createCustomActionsList( CustomBrowseAction browseActions) { ArrayList<Bundle> browseActionsBundle = new ArrayList<>(); for (CustomBrowseAction browseAction : browseActions) { Bundle action = new Bundle(); action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID, browseAction.mId); action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL, getString(browseAction.mLabelResId)); action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI, browseAction.mIcon); browseActionsBundle.add(action); } return browseActionsBundle; }
将自定义浏览操作列表添加到浏览根
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { Bundle browserRootExtras = new Bundle(); browserRootExtras.putParcelableArrayList( BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST, createCustomActionsList())); mRoot = new BrowserRoot(ROOT_ID, browserRootExtras); return mRoot; }
将操作添加到 MediaItem
MediaDescriptionCompat buildDescription (long id, String title, String subtitle, String description, Uri iconUri, Uri mediaUri, ArrayList<String> browseActionIds) { MediaDescriptionCompat.Builder bob = new MediaDescriptionCompat.Builder(); bob.setMediaId(id); bob.setTitle(title); bob.setSubtitle(subtitle); bob.setDescription(description); bob.setIconUri(iconUri); bob.setMediaUri(mediaUri); Bundle extras = new Bundle(); extras.putStringArrayList( DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST, browseActionIds); bob.setExtras(extras); return bob.build(); } MediaItem mediaItem = new MediaItem(buildDescription(...), flags);
构建 onCustomAction
结果
- 从
Bundle extras
解析 mediaId@Override public void onCustomAction( @NonNull String action, Bundle extras, @NonNull Result<Bundle> result){ String mediaId = extras.getString(MediaConstans.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID); }
- 对于异步结果,分离结果。
result.detach()
- 构建结果捆绑包
- 给用户发送消息
mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE, mContext.getString(stringRes))
- 更新项(用于更新项中的操作)
mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM, mediaId);
- 打开播放视图
//Shows user the PBV without changing the playback state mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM, null);
- 更新浏览节点
//Change current browse node to mediaId mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE, mediaId);
- 给用户发送消息
- 如果出现错误,请调用
result.sendError(resultBundle)
。 - 如果是进度更新,请调用
result.sendProgressUpdate(resultBundle)
。 - 最后调用
result.sendResult(resultBundle)
。
更新操作状态
通过将 result.sendProgressUpdate(resultBundle)
方法与 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
键一起使用,您可以更新 MediaItem
以反映操作的新状态。这使您能够向用户提供有关其操作进度和结果的实时反馈。
示例:下载操作
以下是如何使用此功能实现具有三种状态的下载操作的示例
- 下载:这是操作的初始状态。当用户选择此操作时,您可以将其替换为“正在下载”并调用
sendProgressUpdate
来更新 UI。 - 正在下载:此状态表示下载正在进行中。您可以使用此状态向用户显示进度条或其他指示器。
- 已下载:此状态表示下载已完成。当下载完成时,您可以将“正在下载”替换为“已下载”,并调用
sendResult
,使用EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
键指示该项应刷新。此外,您可以使用EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE
键向用户显示成功消息。
这种方法可以为您提供清晰的反馈,让用户了解下载过程及其当前状态。您还可以使用图标添加更多详细信息,以显示 25%、50%、75% 的下载状态。
示例:收藏操作
另一个示例是具有两种状态的收藏操作
- 收藏:此操作显示在用户收藏夹列表中没有的项上。当用户选择此操作时,您可以将其替换为“已收藏”,并调用
sendResult
,使用EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
键来更新 UI。 - 已收藏:此操作显示在用户收藏夹列表中的项上。当用户选择此操作时,您可以将其替换为“收藏”,并调用
sendResult
,使用EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
键来更新 UI。
这种方法为用户管理他们的收藏项提供了一种清晰一致的方式。
这些示例展示了自定义浏览操作的灵活性,以及如何使用它们实现各种功能,并提供实时反馈,从而增强车载媒体应用的用户体验。
有关此功能的完整示例实现,您可以参考 TestMediaApp
项目。
启用播放控制
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 that implements MediaSession.Callback // to handle play control requests. 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 that implements MediaSession.Callback // to handle play control requests. 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 会自动将音频内容发送到汽车系统以通过汽车扬声器播放。
有关播放音频内容的更多信息,请参阅 MediaPlayer 概览、音频应用概览和 ExoPlayer 概览。
设置标准播放操作
Android Auto 和 Android Automotive OS 根据 PlaybackStateCompat
对象中启用的操作显示播放控件。
默认情况下,您的应用必须支持以下操作
如果以下操作与应用的媒体内容相关,您的应用还可以支持它们
此外,您可以选择创建可供用户显示的播放队列,但这不是必需的。为此,请调用 setQueue()
和 setQueueTitle()
方法,启用 ACTION_SKIP_TO_QUEUE_ITEM
操作,并定义回调 onSkipToQueueItem()
。
另外,还要添加对“正在播放”图标的支持,它是当前正在播放内容的指示器。为此,请调用 setActiveQueueItemId()
方法,并传入队列中当前播放项的 ID。每当队列发生变化时,您都需要更新 setActiveQueueItemId()
。
Android Auto 和 Android Automotive OS 会显示每个已启用操作的按钮以及播放队列。点击按钮时,系统会调用 MediaSessionCompat.Callback
中对应的回调。
保留未使用的空间
Android Auto 和 Android Automotive OS 会在界面中为 ACTION_SKIP_TO_PREVIOUS
和 ACTION_SKIP_TO_NEXT
操作预留空间。如果您的应用不支持其中一项功能,Android Auto 和 Android Automotive OS 会使用该空间显示您创建的任何自定义操作。
如果您不想用自定义操作填充这些空间,可以保留它们,以便 Android Auto 和 Android Automotive OS 在您的应用不支持相应功能时将空间留空。为此,请调用 setExtras()
方法,并传入一个包含与保留功能对应的常量的 extras bundle。SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
对应于 ACTION_SKIP_TO_NEXT
,SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
对应于 ACTION_SKIP_TO_PREVIOUS
。将这些常量用作 bundle 中的键,并将布尔值 true
用作它们的值。
设置初始 PlaybackState
当 Android Auto 和 Android Automotive OS 与您的媒体浏览器服务通信时,您的媒体会话会使用 PlaybackStateCompat
传达内容播放状态。当 Android Automotive OS 或 Android Auto 连接到您的媒体浏览器服务时,您的应用不应自动开始播放音乐。相反,应依靠 Android Auto 和 Android Automotive OS 根据汽车的状态或用户操作恢复或开始播放。
为此,请将媒体会话的初始 PlaybackStateCompat
设置为 STATE_STOPPED
、STATE_PAUSED
、STATE_NONE
或 STATE_ERROR
。
Android Auto 和 Android Automotive OS 中的媒体会话仅在驾驶期间持续,因此用户会频繁启动和停止这些会话。为了促进驾驶之间的无缝体验,请跟踪用户的上一个会话状态,以便当媒体应用收到恢复请求时,用户可以自动从上次离开的地方继续播放,例如上次播放的媒体项、PlaybackStateCompat
和队列。
添加自定义播放操作
您可以添加自定义播放操作以显示您的媒体应用支持的其他操作。如果空间允许(且未保留),Android 会将自定义操作添加到传输控件。否则,自定义操作会显示在溢出菜单中。自定义操作按添加到 PlaybackStateCompat
的顺序显示。
使用自定义操作提供与标准操作不同的行为。不要使用它们来替换或复制标准操作。
您可以使用 addCustomAction()
方法在 PlaybackStateCompat.Builder
类中添加自定义操作。
以下代码片段展示了如何添加自定义“启动电台频道”操作
Kotlin
val customActionExtras = Bundle() customActionExtras.putInt( androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, androidx.media3.session.CommandButton.ICON_RADIO) stateBuilder.addCustomAction( PlaybackStateCompat.CustomAction.Builder( CUSTOM_ACTION_START_RADIO_FROM_MEDIA, resources.getString(R.string.start_radio_from_media), startRadioFromMediaIcon // or R.drawable.media3_icon_radio ).run { setExtras(customActionExtras) build() } )
Java
Bundle customActionExtras = new Bundle(); customActionExtras.putInt( androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, androidx.media3.session.CommandButton.ICON_RADIO); stateBuilder.addCustomAction( new PlaybackStateCompat.CustomAction.Builder( CUSTOM_ACTION_START_RADIO_FROM_MEDIA, resources.getString(R.string.start_radio_from_media), startRadioFromMediaIcon) // or R.drawable.media3_icon_radio .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
方法。
自定义操作的图标
您创建的每个自定义操作都需要一个图标。
如果该图标的描述与 CommandButton.ICON_
常量之一匹配,则应将该整数值设置为自定义操作 extras 的 EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT
键。在受支持的系统上,这将覆盖传递给 CustomAction.Builder
的图标资源,从而允许系统组件以一致的样式渲染您的操作和其他播放操作。
您还必须指定一个图标资源。汽车中的应用可以在许多不同的屏幕尺寸和密度上运行,因此您提供的图标必须是矢量可绘制对象。矢量可绘制对象允许您缩放资源而不会丢失细节。矢量可绘制对象还可以轻松地在较低分辨率下将边缘和角对齐到像素边界。
如果自定义操作是有状态的(例如,它切换播放设置的开/关),请为不同的状态提供不同的图标,以便用户在选择该操作时能看到变化。
为禁用操作提供替代图标样式
当自定义操作在当前上下文中不可用时,请将自定义操作图标替换为显示该操作已禁用的替代图标。
指示音频格式
为了指示当前播放的媒体使用特殊音频格式,您可以指定在支持此功能的汽车中渲染的图标。您可以将 KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI
和 KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI
设置在当前播放的媒体项的 extras bundle 中(传入 MediaSession.setMetadata()
)。请确保同时设置这两个 extras,以适应不同的布局。
此外,您可以设置 KEY_IMMERSIVE_AUDIO
extra,告知汽车 OEM 这是沉浸式音频,他们在决定是否应用可能干扰沉浸式内容的音频效果时应格外小心。
从当前播放项添加链接
您可以配置当前播放的媒体项,使其副标题、描述或两者都成为指向其他媒体项的链接。这允许用户快速跳转到相关项;例如,他们可以跳转到同一艺术家的其他歌曲、该播客的其他剧集等。如果汽车支持此功能,用户可以点击链接浏览到该内容。
要添加链接,请配置 KEY_SUBTITLE_LINK_MEDIA_ID
元数据(用于从副标题链接)或 KEY_DESCRIPTION_LINK_MEDIA_ID
(用于从描述链接)。有关详细信息,请参阅这些元数据字段的参考文档。
支持语音操作
您的媒体应用必须支持语音操作,以帮助驾驶员获得安全便捷且最大限度减少干扰的体验。例如,如果您的应用正在播放一个媒体项,用户可以说“播放 [歌曲标题]”来告诉您的应用播放不同的歌曲,而无需查看或触摸汽车显示屏。用户可以通过点击方向盘上的相应按钮或说出热词“OK Google”来启动查询。
当 Android Auto 或 Android Automotive OS 检测并解释语音操作时,该语音操作会通过 onPlayFromSearch()
传递给应用。收到此回调后,应用会查找与 query
字符串匹配的内容并开始播放。
用户可以在其查询中指定不同类别的术语:流派、艺术家、专辑、歌曲名称、广播电台或播放列表等。在构建搜索支持时,请考虑所有适用于您的应用的类别。如果 Android Auto 或 Android Automotive OS 检测到给定查询符合某些类别,它会在 extras
参数中附加 extras。可以发送以下 extras
考虑空 query
字符串,Android Auto 或 Android Automotive OS 可以在用户未指定搜索词时发送此字符串。例如,如果用户说“播放一些音乐”。在这种情况下,您的应用可能会选择开始播放最近播放的或新建议的曲目。
如果搜索无法快速处理,请不要在 onPlayFromSearch()
中阻塞。相反,将播放状态设置为 STATE_CONNECTING
并在异步线程上执行搜索。
播放开始后,考虑使用相关内容填充媒体会话的队列。例如,如果用户请求播放一张专辑,您的应用可能会用专辑的曲目列表填充队列。另外,请考虑实现对可浏览搜索结果的支持,以便用户可以选择与查询匹配的不同曲目。
除了“播放”查询之外,Android Auto 和 Android Automotive OS 还会识别用于控制播放的语音查询,例如“暂停音乐”和“下一首歌曲”,并将这些命令与适当的媒体会话回调匹配,例如 onPause()
和 onSkipToNext()
。
有关如何在应用中实现启用语音的播放操作的详细示例,请参阅 Google 助理和媒体应用。
实施分心保护措施
由于用户在使用 Android Auto 时手机连接到汽车扬声器,因此您必须采取额外的预防措施来帮助防止驾驶员分心。
抑制车内闹钟
除非用户通过按播放按钮等方式开始播放,否则 Android Auto 媒体应用不得通过汽车扬声器播放音频。即使是您的媒体应用的用户预设闹钟也不得通过汽车扬声器播放音乐。
为了满足此要求,您的应用可以在播放任何音频之前使用 CarConnection
作为信号。您的应用可以通过观察 汽车连接类型的 LiveData
并检查其是否等于 CONNECTION_TYPE_PROJECTION
来检查手机是否正在投射到汽车屏幕。
如果用户手机正在投射,支持闹钟的媒体应用必须执行以下操作之一
- 禁用闹钟。
- 通过
STREAM_ALARM
播放闹钟,并在手机屏幕上提供一个 UI 来禁用闹钟。
处理媒体广告
默认情况下,当音频播放会话期间媒体元数据发生变化时,Android Auto 会显示通知。当媒体应用从播放音乐切换到播放广告时,显示通知会分散用户注意力。为了防止 Android Auto 在此情况下显示通知,您必须将媒体元数据键 METADATA_KEY_IS_ADVERTISEMENT
设置为 METADATA_VALUE_ATTRIBUTE_PRESENT
,如以下代码片段所示
Kotlin
import androidx.media.utils.MediaConstants override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) { MediaMetadataCompat.Builder().apply { if (isAd(mediaId)) { putLong( MediaConstants.METADATA_KEY_IS_ADVERTISEMENT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT) } // ...add any other properties you normally would. mediaSession.setMetadata(build()) } }
Java
import androidx.media.utils.MediaConstants; @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); if (isAd(mediaId)) { builder.putLong( MediaConstants.METADATA_KEY_IS_ADVERTISEMENT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT); } // ...add any other properties you normally would. mediaSession.setMetadata(builder.build()); }
处理一般错误
当应用遇到错误时,将播放状态设置为 STATE_ERROR
,并使用 setErrorMessage()
方法提供错误消息。请参阅 PlaybackStateCompat
,了解设置错误消息时可以使用的错误代码列表。错误消息必须面向用户,并根据用户的当前区域设置进行本地化。然后,Android Auto 和 Android Automotive OS 可以向用户显示错误消息。
例如,如果内容在用户当前地区不可用,您可以在设置错误消息时使用 ERROR_CODE_NOT_AVAILABLE_IN_REGION
错误代码。
Kotlin
mediaSession.setPlaybackState( PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_ERROR) .setErrorMessage(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION, getString(R.string.error_unsupported_region)) // ...and any other setters. .build())
Java
mediaSession.setPlaybackState( new PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_ERROR) .setErrorMessage(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION, getString(R.string.error_unsupported_region)) // ...and any other setters. .build());
有关错误状态的更多信息,请参阅使用媒体会话:状态和错误。
如果 Android Auto 用户需要打开您的手机应用来解决错误,请在您的消息中向用户提供该信息。例如,您的错误消息可以说“登录 [您的应用名称]”,而不是“请登录”。