Android Auto 和 Android Automotive OS 可帮助您将媒体应用内容带给车上的用户。车载媒体应用必须提供媒体浏览器服务,Android Auto 和 Android Automotive OS 或其他带有媒体浏览器的应用才能发现和展示您的内容。
本指南假定您已经拥有可在手机上播放音频的媒体应用,并且您的媒体应用遵循 Android 媒体应用架构。
本指南介绍了您的应用在 Android Auto 或 Android Automotive OS 上运行所需的 MediaBrowserService
和 MediaSession
组件。完成核心媒体基础架构的搭建后,您可以向媒体应用添加对 Android Auto 的支持,以及添加对 Android Automotive OS 的支持。
准备工作
- 查看 Android 媒体 API 文档。
- 查看制作媒体应用以获取设计指南。
- 查看本部分列出的关键术语和概念。
关键术语和概念
- 媒体浏览器服务
- 由您的媒体应用实现且符合
MediaBrowserServiceCompat
API 要求的 Android 服务。应用使用此服务来公开其内容。 - 媒体浏览器
- 媒体应用用于发现媒体浏览器服务并展示其内容的 API。Android Auto 和 Android Automotive OS 使用媒体浏览器查找应用的媒体浏览器服务。
- 媒体项
媒体浏览器将其内容整理成
MediaItem
对象树。媒体项可以包含以下两个标记之一,也可以同时包含这两个标记:FLAG_PLAYABLE
:表示媒体项为内容树上的一个叶项。这类媒体项表示单个声音流,如专辑里的一首歌曲、有声读物中的一章或一集播客。FLAG_BROWSABLE
:表示媒体项为内容树上的一个节点,它有子项。例如,媒体项表示一个专辑,其子项为专辑里的歌曲。
既可浏览又可播放的媒体项相当于播放列表。您可以选择媒体项本身来播放其中的所有子项,也可以浏览其子项。
- 车辆优化
遵循 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 用于在系统界面中表示您的应用。必须提供的两种图标:
- 启动器图标
- 提供方图标
启动器图标
启动器图标用于在系统界面中表示您的应用,例如在启动器和图标托盘中。您可以使用以下清单声明,说明要使用您移动应用中的图标来表示您的汽车媒体应用。
<application
...
android:icon="@mipmap/ic_launcher"
...
/>
如果要使用的图标与移动应用的图标不同,请在清单中为您的媒体浏览器服务的 <service>
元素设置 android:icon
属性:
<application>
...
<service
...
android:icon="@mipmap/auto_launcher"
...
/>
</application>
提供方图标
![](https://developer.android.google.cn/static/images/training/cars/attribution-icon.png?authuser=0000&hl=ro)
图 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); }
如需查看此方法的详细示例,请参阅 onGetRoot()
方法。
为 onGetRoot() 添加软件包验证
调用服务的 onGetRoot()
方法时,发出调用的软件包会将标识信息传递给您的服务。您的服务可以使用此信息来决定该软件包能否访问您的内容。例如,您可以将 clientPackageName
与许可名单进行比较,并验证用于为软件包的 APK 签名的证书,从而限制只有获批的软件包有权访问应用的内容。如果无法验证软件包,则返回 null
以拒绝它访问您的内容。
为了允许系统应用(例如 Android Auto 和 Android Automotive OS)访问您的内容,当这些系统应用调用 onGetRoot()
方法时,您的服务应始终返回一个非 null 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
}
此代码段摘录自 PackageValidator
类。请参阅该类,通过更详细的示例来了解如何为服务的 onGetRoot()
方法实现软件包验证。
除了允许系统应用外,您还必须允许 Google 助理关联到您的 MediaBrowserService
。请注意,在手机(包括 Android Auto)和 Android Automotive OS 上,Google 助理具有单独的软件包名称。
实现 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); }
有关此方法的完整示例,请参阅
onLoadChildren()
方法。
构造根菜单
![](https://developer.android.google.cn/static/images/training/cars/browse_home_tabs.png?authuser=0000&hl=ro)
图 2. 根内容显示为导航标签页。
Android Auto 和 Android Automotive OS 对根菜单的结构有特定的约束条件。这些约束条件通过根提示传达给 MediaBrowserService
,而根提示可通过传入 onGetRoot()
的 Bundle
参数来读取。遵循这些提示可让系统以最佳方式将根内容显示为导航标签页。如果您不遵循这些提示,某些根内容可能会被系统丢弃或不太容易被系统发现。会发送两个提示:
- 根子项的数量限制:在大多数情况下,此数字应该为 4。这意味着,不能显示超过 4 个标签页。
- 根子项的受支持标志:此值应该为
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 必须解析为应用资源中的位图或矢量可绘制对象。对于表示内容层次结构中的项的 MediaDescriptionCompat
对象,通过 setIconUri()
传递 URI。对于表示当前播放项的 MediaMetadataCompat
对象,使用以下任意键,通过 putString()
传递 URI:
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI
MediaMetadataCompat.METADATA_KEY_ART_URI
MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI
以下步骤说明了如何通过网页 URI 下载图片并通过本地 URI 公开图片。有关更完整的示例,请参阅
实现
的 openFile()
以及 Universal Android Music 中的相关方法
播放器示例应用。
构建与 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); }
如需详细了解 content provider,请参阅创建 content provider。
采用内容样式
使用可浏览项或可播放项构建内容层次结构后,您可以应用内容样式来确定这些项在汽车中的显示方式。
您可以采用以下内容样式:
- 列表项
-
此内容样式重点显示标题和元数据,其次是图片。
- 网格项
-
在此内容样式下,图片的优先级高于标题和元数据。
设置默认内容样式
您可以通过在服务的 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
:相应项以“类别”列表项的形式呈现。这些项与普通列表项相同,只不过会在这些项的图标周围应用外边距,因为小图标的视觉效果更好。这些图标必须是可着色的矢量可绘制对象。此提示应仅针对可浏览项提供。DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM
:相应项以“类别”网格项的形式呈现。这些项与普通网格项相同,只不过会在这些项的图标周围应用外边距,因为小图标的视觉效果更好。这些图标必须是可着色的矢量可绘制对象。此提示应仅针对可浏览项提供。
以下代码段展示了如何将可浏览项的默认内容样式设置为网格,并将可播放项的默认内容样式设置为列表:
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); }
按项设置内容样式
您可以通过 Content Style API,替换任何可浏览媒体项的子项以及任何媒体项本身的默认内容样式。
如需为可浏览媒体项的子项替换默认项,请在媒体项的 MediaDescription
中创建 extra 包,并添加上述相同的提示。DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE
适用于该项的可播放子项,而 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE
适用于该项的可浏览子项。
如需替换特定媒体项本身(而非其子项)的默认值,请在媒体项的 MediaDescription
中创建 extra 包,并使用键 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
中声明一个 extra 包,其中包含与 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*/); }
您的应用必须将您想要归入一组的所有媒体项传递为一个连续的块。例如,假设您希望显示“Songs”和“Albums”这两组媒体项(按此顺序),并且您的应用按以下顺序传递了五个媒体项:
- 媒体项 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")
由于“Songs”组和“Albums”组中的媒体项未归入连续的块中,因此 Android Auto 和 Android Automotive OS 会将其解读为以下四组:
- 第 1 组名为“Songs”,包含媒体项 A
- 第 2 组名为“Albums”,包含媒体项 B
- 第 3 组名为“Songs”,包含媒体项 C 和 D
- 第 4 组名为“Albums”,包含媒体项 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 会读取与项关联的 extra,并查找特定常量来确定要显示的指示器。在媒体播放期间,Android Auto 和 Android Automotive OS 会读取媒体会话的元数据,并查找特定常量来确定要显示的指示器。
![](https://developer.android.google.cn/static/images/training/cars/metadata_indicators.png?authuser=0000&hl=ro)
图 3. 一个播放视图,包含用于识别歌曲和艺人的元数据,以及用于指示露骨内容的图标。
![](https://developer.android.google.cn/static/images/training/cars/progress_indicators.png?authuser=0000&hl=ro)
图 4. 一个浏览视图,在第一个项目上用一个点表示未播放内容,并在第二个项目上用一个进度条表示部分播放的内容。
以下常量可同时用于 MediaItem
说明 extra 和 MediaMetadata
extra:
EXTRA_DOWNLOAD_STATUS
:表示项的下载状态。将此常量用作键,以下长常量为可能的值:STATUS_DOWNLOADED
:项已下载完毕。STATUS_DOWNLOADING
:正在下载项。STATUS_NOT_DOWNLOADED
:未下载项。
METADATA_KEY_IS_EXPLICIT
:表明项是否包含露骨内容。如需表明某个项为露骨内容,请使用此常量作为键,并使用长常量METADATA_VALUE_ATTRIBUTE_PRESENT
作为值。
以下常量只能在 MediaItem
说明 extra 中使用:
DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS
:表示较长内容(例如播客分集或有声读物)是否播放完毕。将此常量用作键,以下整数常量为可能的值:DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE
:用 0.0 到 1.0(含)之间的双精度数值表示长片内容的播放进度量。此 extra 会提供有关PARTIALLY_PLAYING
状态的更多信息,因此 Android Auto 或 Android Automotive OS 会显示更有意义的进度指示器,例如进度条。如果您使用此 extra,请参阅本指南上的在内容播放时更新浏览视图中的进度条,了解如何使此指示器在初始展示后保持最新状态。
为了显示在用户浏览媒体浏览树时出现的指示器,可创建一个 extra 包,在其中加入一个或多个常量,然后将该包传递给 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
必须在其 extra 中发送DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE
,其值介于 0.0 和 1.0(含)之间。 MediaMetadataCompat
必须发送METADATA_KEY_MEDIA_ID
,且字符串值等于传递到MediaItem
的媒体 ID。PlaybackStateCompat
必须包含一个键为PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID
的 extra,其映射到与传递到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());
显示可浏览的搜索结果
![](https://developer.android.google.cn/static/images/training/cars/search_results.png?authuser=0000&hl=ro)
图 5. 一个播放视图,包含“Search results”选项,用于查看与用户的语音搜索相关的媒体项。
您的应用可以提供在用户发起搜索查询时向其显示的上下文搜索结果。Android Auto 和 Android Automotive OS 将通过搜索查询接口或通过基于会话中之前进行的查询的功能显示这些结果。如需了解详情,请参阅本指南中的支持语音操作部分。
如需显示可浏览的搜索结果,请在您服务的 onGetRoot()
方法的 extra 包中添加常量键 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()
方法。每当用户调用搜索查询接口或“Search results”功能时,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. }
自定义浏览操作
![单个自定义浏览操作。](https://developer.android.google.cn/static/images/training/cars/custom_browse_action_single.png?authuser=0000&hl=ro)
图 6. 单个自定义浏览操作
自定义浏览操作允许您将自定义图标和标签添加到应用的
MediaItem
对象,并处理用户与以下各项的互动:
这些操作。这样,您就可以扩展媒体应用的功能,
例如添加“下载”“添加到队列”“播放电台”
“收藏”或“移除”操作。
![自定义浏览操作菜单。](https://developer.android.google.cn/static/images/training/cars/custom_browse_action_overflow.png?authuser=0000&hl=ro)
图 7. 自定义浏览操作溢出
如果自定义操作的数量超出 OEM 允许的数量, 向用户展示菜单
运作方式
每个自定义浏览操作均通过以下各项进行定义:
- 操作 ID(唯一字符串标识符)
- 操作标签(向用户显示的文字)
- 操作图标 URI(可着色的矢量可绘制对象)
您可以在
BrowseRoot
。然后,您可以将这些操作中的一部分附加到单个
MediaItem.
当用户与自定义浏览操作互动时,您的应用会收到回调
在 onCustomAction()
中。然后,您可以处理该操作并更新
对 MediaItem
执行的操作(如有必要)。这对于有状态操作非常有用
如“收藏夹”和“下载”执行不需要更新的操作,例如“播放”
收视活动”,则无需更新操作列表。
![浏览节点根目录中的自定义浏览操作。](https://developer.android.google.cn/static/images/training/cars/custom_browse_action_toolbar.png?authuser=0000&hl=ro)
图 8. 自定义浏览操作工具栏
您还可以将自定义浏览操作附加到浏览节点根目录。这些操作 会显示在主工具栏下的辅助工具栏中。
如何实现自定义浏览操作
如需向项目添加自定义浏览操作,请按以下步骤操作:
- 替换
MediaBrowserServiceCompat
实现: - 解析运行时的操作限制:
<ph type="x-smartling-placeholder">
- </ph>
- 在
onGetRoot()
中,获取每项允许的操作数量上限MediaItem
(使用密钥)BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT
在rootHints
Bundle
中。限值为 0 表示该地图项 。
- 在
- 构建自定义浏览操作的全局列表:
<ph type="x-smartling-placeholder">
- </ph>
- 对于每项操作,请创建一个包含以下键的
Bundle
对象: *EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID
:操作 ID *EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL
:操作标签 *EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI
:操作图标 URI * 将所有操作Bundle
对象添加到列表中。
- 对于每项操作,请创建一个包含以下键的
- 将全局列表添加到您的
BrowseRoot
中: <ph type="x-smartling-placeholder">- </ph>
- 在
BrowseRoot
extraBundle
中,将操作列表添加为Parcelable
Arraylist
正在使用该密钥BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
。
- 在
- 向
MediaItem
对象添加操作: <ph type="x-smartling-placeholder">- </ph>
- 您可以通过添加
MediaItem
MediaDescriptionCompat
extra 中使用键的操作 ID 列表DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
。 此列表必须是您在BrowseRoot
。
- 您可以通过添加
- 处理操作并返回进度或结果:
<ph type="x-smartling-placeholder">
- </ph>
- 在
onCustomAction
中,根据操作 ID 和任何 所需的其他数据您可以从中MediaItem
获取 使用键从 extra 触发操作EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID
。 - 您可以通过添加
MediaItem
密钥EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
进度或结果包中
- 在
要开始使用,您可以在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()
- 构建结果软件包
<ph type="x-smartling-placeholder">
- </ph>
- 发送给用户的消息
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
用于更新界面。 - 正在下载:此状态表示正在下载。您可以 使用此状态向用户显示进度条或其他指示器。
- 已下载:此状态表示下载已完成。当
您可以把“下载”显示“已下载”并致电
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
键以更新界面。 - 已收藏:对用户的
收藏夹。当用户选择此操作后,您可以将其替换为
“收藏”并使用
sendResult
EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
键以更新界面。
这种方法为用户提供了一种清晰且一致的方法来管理其 收藏的内容。
这些示例展示了自定义浏览操作的灵活性,以及如何 使用它们实现各种功能,并针对 从而提升车载媒体应用中的用户体验。
有关此功能的完整实现示例,您可以参阅
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 会使对应空间留空。为此,请创建 extra 包以包含与保留功能对应的常量,然后使用这个 extra 包来调用 setExtras()
方法。SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
对应于 ACTION_SKIP_TO_NEXT
,SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
对应于 ACTION_SKIP_TO_PREVIOUS
。使用这些常量作为包中的键,并使用布尔值 true
作为它们的值。
设置初始播放状态
当 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
中的顺序进行显示。
使用自定义操作来提供与标准操作不同的行为。请勿使用此类操作来替换或重复标准操作。
您可以使用 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());
如需查看此方法的详细示例,请参阅 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)) { ... } }
如需查看此方法的详细示例,请参阅 onCustomAction
方法。
自定义操作的图标
您创建的每个自定义操作都需要使用图标资源。车载应用可能会以许多不同的屏幕尺寸和密度来运行,因此您提供的图标必须为矢量可绘制对象。矢量可绘制对象允许缩放资源,同时又不会丢失细节。矢量可绘制对象还能以较小的分辨率轻松地将边和角对准像素边界。
如果某项自定义操作是有状态的(例如,可打开或关闭一项播放设置),请为不同的状态提供不同的图标,这样当用户选择该操作时便可看到变化。
为已停用的操作提供替代图标样式
如果自定义操作不适用于当前上下文,可将自定义操作图标替换为表明操作已被停用的替代图标。
指明音频格式
为了指明当前播放的媒体使用特殊的音频格式,您可以指定在支持此功能的汽车上呈现相应图标。您可以在当前播放的媒体项(传递给 MediaSession.setMetadata()
)的 extra 包中设置 KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI
和 KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI
。为了适应不同的布局,请务必同时设置这些 extra。
此外,您可以设置 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
参数中附加 extra。可以发送以下 extra:
考虑使用一个空的 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
播放闹钟铃声,并在手机屏幕上提供用于停用闹钟的界面。
处理媒体广告
默认情况下,当媒体元数据在音频播放会话期间发生更改时,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 用户需要打开您的手机应用以解决错误,请在您的消息中向用户提供相应信息。例如,您的错误消息可显示“登录 [您的应用名称]”,而不是“请登录”。