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 文档。
- 查看 创建媒体应用 以获取设计指南。
- 查看本节中列出的关键术语和概念。
关键术语和概念
- 媒体浏览器服务
- 您的媒体应用实现的 Android 服务,符合
MediaBrowserServiceCompat
API。您的应用使用此服务公开其内容。 - 媒体浏览器
- 媒体应用用来发现媒体浏览器服务并显示其内容的 API。Android Auto 和 Android Automotive OS 使用媒体浏览器查找您的应用的媒体浏览器服务。
- 媒体项目
媒体浏览器将其内容组织在一个
MediaItem
对象树中。媒体项目可以具有以下一个或两个标志FLAG_PLAYABLE
:指示该项目是内容树上的叶子。该项目表示单个声音流,例如专辑中的歌曲、有声读物中的章节或播客的剧集。FLAG_BROWSABLE
:指示该项目是内容树上的节点,并且它具有子节点。例如,该项目表示专辑,其子节点是专辑中的歌曲。
既可浏览又可播放的媒体项目充当播放列表。您可以选择该项目本身来播放其所有子节点,也可以浏览其子节点。
- 车辆优化
Android Automotive OS 应用的活动,符合 Android Automotive OS 设计指南。这些活动的界面不是由 Android Automotive OS 绘制的,因此您必须确保您的应用符合设计指南。通常,这包括更大的点击目标和字体大小、对昼夜模式的支持以及更高的对比度。
车辆优化用户界面仅在汽车用户体验限制 (CUXR) 不生效时才允许显示,因为这些界面可能需要用户长时间关注或交互。当汽车停止或停放时,CUXR 不生效,但在汽车行驶时始终生效。
您不需要为 Android Auto 设计活动,因为 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 可用于在系统 UI 中表示您的应用的应用图标。需要两种图标类型
- 启动器图标
- 归属图标
启动器图标
启动器图标在系统 UI 中表示您的应用,例如在启动器和图标托盘中。您可以指定要使用手机应用中的图标来表示您的汽车媒体应用,方法是使用以下清单声明
<application
...
android:icon="@mipmap/ic_launcher"
...
/>
要使用与手机应用不同的图标,请在清单中媒体浏览器服务的<service>
元素上设置android:icon
属性
<application>
...
<service
...
android:icon="@mipmap/auto_launcher"
...
/>
</application>
归属图标
归属图标用于媒体内容优先的位置,例如媒体卡片。考虑重新使用用于通知的小图标。此图标必须为单色。您可以指定用于表示您的应用的图标,方法是使用以下清单声明
<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()
方法时返回非 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
}
此代码片段摘自 GitHub 上通用 Android 音乐播放器示例应用中的PackageValidator
类。有关如何为服务的onGetRoot()
方法实现包验证的更详细示例,请参阅该类。
除了允许系统应用外,您还必须允许 Google 助理连接到您的MediaBrowserService
。请注意,Google 助理对手机(包括 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()
方法。
构建根菜单结构
Android Auto 和 Android Automotive OS 对根菜单的结构有一些特定的限制。这些限制通过根提示(root hints)传达给 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
集成之间有所不同时。例如,如果您通常显示一个可播放的根项目,则可能需要将其嵌套在可浏览的根项目下,这取决于支持的标志提示的值。
除了根提示之外,还有几个其他指南需要遵循,以帮助确保选项卡以最佳方式呈现
- 为每个选项卡项目提供单色(最好是白色)图标。
- 为每个选项卡项目提供简短但有意义的标签。保持标签简短可以减少字符串被截断的可能性。
显示媒体图片
媒体项目的图片必须作为本地 URI 传递,可以使用 ContentResolver.SCHEME_CONTENT
或 ContentResolver.SCHEME_ANDROID_RESOURCE
。此本地 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 公开它。有关更完整的示例,请参阅 Universal Android Music Player 示例应用程序中 openFile()
及其周围方法的 实现。
构建一个对应于网络 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
附加信息包中包含某些常量来设置媒体项目显示方式的全局默认值。Android Auto 和 Android Automotive OS 会读取此包并查找这些常量以确定合适的样式。
以下附加信息可作为包中的键使用
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); }
设置每个项目的样式
内容样式 API 允许您覆盖任何可浏览媒体项目的子级以及任何媒体项目本身的默认内容样式。
要覆盖可浏览媒体项目的子级的默认样式,请在媒体项目的 MediaDescription
中创建一个附加信息包,并添加前面提到的相同提示。 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE
应用于该项目的可播放子级,而 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE
应用于该项目的可浏览子级。
要覆盖特定媒体项目本身(而不是其子级)的默认样式,请在媒体项目的 MediaDescription
中创建一个附加信息包,并添加一个键为 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
中声明一个附加信息包,其中包含一个键为 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 读取与项目关联的额外信息,并查找某些常量以确定要显示哪些指示器。在媒体播放期间,Android Auto 和 Android Automotive OS 读取媒体会话的元数据,并查找某些常量以确定要显示的指示器。
以下常量可用于两者 MediaItem
描述额外信息和 MediaMetadata
额外信息中
EXTRA_DOWNLOAD_STATUS
:指示项目的下载状态。使用此常量作为键;以下长常量是可能的值STATUS_DOWNLOADED
:项目已完全下载。STATUS_DOWNLOADING
:正在下载项目。STATUS_NOT_DOWNLOADED
:项目未下载。
METADATA_KEY_IS_EXPLICIT
:指示项目是否包含显式内容。要指示项目是显式的,请使用此常量作为键,并使用长METADATA_VALUE_ATTRIBUTE_PRESENT
作为值。
以下常量只能用于 MediaItem
描述额外信息中
DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS
:指示长格式内容(例如播客剧集或有声读物)的完成状态。使用此常量作为键;以下整数常量是可能的值DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE
:指示长格式内容的完成进度量,以介于 0.0 和 1.0(含)之间的双精度数表示。此额外信息提供了有关PARTIALLY_PLAYING
状态的更多信息,以便 Android Auto 或 Android Automotive OS 显示更有意义的进度指示器,例如进度条。如果您使用此额外信息,请参阅本指南中有关在浏览视图中更新进度条以显示内容播放的部分,了解如何在首次显示后保持此指示器最新。
要显示用户在浏览媒体浏览树时出现的指示器,请创建一个包含一个或多个这些常量的额外信息包,并将该包传递给 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
额外信息在浏览视图中显示部分播放内容的进度条。但是,如果用户从 Android Auto 或 Android Automotive OS 继续播放部分播放的内容,则随着时间的推移,该指示器会变得不准确。
为了使 Android Auto 和 Android Automotive OS 保持进度条最新,您可以在 MediaMetadataCompat
和 PlaybackStateCompat
中提供其他信息,以将正在进行的内容链接到浏览视图中的媒体项目。要使媒体项目具有自动更新的进度条,必须满足以下要求
- 创建时,
MediaItem
必须在其额外信息中发送DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE
,其值介于 0.0 和 1.0(含)之间。 MediaMetadataCompat
必须发送METADATA_KEY_MEDIA_ID
,其字符串值等于传递给MediaItem
的 媒体 ID。PlaybackStateCompat
必须包含一个额外信息,其键为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());
显示可浏览的搜索结果
您的应用可以提供上下文搜索结果,当用户发起搜索查询时显示给用户。Android Auto 和 Android Automotive OS 通过搜索查询界面或通过围绕会话早期发出的查询的辅助功能显示这些结果。要了解更多信息,请参阅本指南中的支持语音操作部分。
要显示可浏览的搜索结果,请在服务的 onGetRoot()
方法的额外信息包中包含常量键 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. }
自定义浏览操作
自定义浏览操作允许您在汽车的媒体应用中向应用的 MediaItem
对象添加自定义图标和标签,并处理用户与这些操作的交互。这使您可以通过多种方式扩展媒体应用的功能,例如添加“下载”、“添加到队列”、“播放广播”、“收藏”或“删除”操作。
如果自定义操作的数量超过 OEM 允许显示的数量,则会向用户显示溢出菜单。
它们是如何工作的?
每个自定义浏览操作都由以下内容定义:
- 操作 ID(唯一的字符串标识符)
- 操作标签(显示给用户的文本)
- 操作图标 URI(可以着色的矢量可绘制对象)
您将自定义浏览操作列表全局定义为 BrowseRoot
的一部分。然后,您可以将这些操作的子集附加到各个 MediaItem
中。
当用户与自定义浏览操作交互时,您的应用将在onCustomAction()
中收到回调。然后,您可以处理该操作,并在必要时更新MediaItem
的操作列表。这对于“收藏”和“下载”等有状态的操作很有用。对于不需要更新的操作(例如“播放广播”),您无需更新操作列表。
您还可以将自定义浏览操作附加到浏览节点根目录。这些操作将显示在主工具栏下方的辅助工具栏中。
如何实现自定义浏览操作
以下是将自定义浏览操作添加到项目的步骤
- 覆盖
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 中使用键EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID
.获取触发操作的MediaItem
的 ID。 - 您可以通过在进度或结果 bundle 中包含键
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()
- 构建结果 bundle
- 向用户显示的消息
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。 - 正在下载:此状态表示下载正在进行。您可以使用此状态向用户显示进度条或其他指示器。
- 已下载:此状态表示下载已完成。下载完成后,您可以将“正在下载”替换为“已下载”,并使用键
EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
调用sendResult
,以指示应刷新该项目。此外,您可以使用EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE
键向用户显示成功消息。
这种方法使您可以向用户提供有关下载过程及其当前状态的清晰反馈。您可以使用图标添加更多详细信息以显示 25%、50%、75% 的下载状态。
示例:收藏操作
另一个示例是具有两种状态的收藏操作
- 收藏:此操作显示在不在用户收藏列表中的项目中。当用户选择此操作时,您可以将其替换为“已收藏”,并使用键
EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
调用sendResult
以更新 UI。 - 已收藏:此操作显示在用户收藏列表中的项目中。当用户选择此操作时,您可以将其替换为“收藏”,并使用键
EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
调用sendResult
以更新 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 会在 UI 中为ACTION_SKIP_TO_PREVIOUS
和ACTION_SKIP_TO_NEXT
操作预留空间。如果您的应用不支持其中一项功能,则 Android Auto 和 Android Automotive OS 会使用该空间显示您创建的任何自定义操作。
如果您不想用自定义操作填充这些空间,您可以预留它们,以便 Android Auto 和 Android Automotive OS 在您的应用不支持相应功能时使该空间留空。为此,请使用包含对应于预留功能的常量的额外捆绑包调用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
作为其值。
设置初始 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
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 上的通用 Android 音乐播放器示例应用中的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 上的通用 Android 音乐播放器示例应用中的onCustomAction
方法。
自定义操作的图标
您创建的每个自定义操作都需要一个图标资源。汽车中的应用可以在许多不同的屏幕尺寸和密度下运行,因此您提供的图标必须是矢量可绘制对象。矢量可绘制对象允许您缩放资源而不会丢失细节。矢量可绘制对象还可以轻松地将边缘和角对齐到较小分辨率下的像素边界。
如果自定义操作是有状态的(例如,它会打开或关闭播放设置),请为不同的状态提供不同的图标,以便用户在选择操作时可以看到变化。
为禁用的操作提供备用图标样式
当自定义操作在当前上下文中不可用时,请将自定义操作图标替换为显示该操作已禁用的备用图标。
指示音频格式
要指示当前播放的媒体使用特殊音频格式,您可以指定在支持此功能的汽车中呈现的图标。您可以在当前播放的媒体项目的额外捆绑包中设置KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI
和KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI
(传递到MediaSession.setMetadata()
)。请确保设置这两个额外内容,以适应不同的布局。
此外,您可以设置KEY_IMMERSIVE_AUDIO
额外内容以告知汽车 OEM 这是沉浸式音频,并且在决定是否应用可能干扰沉浸式内容的音频效果时,他们应该非常小心。
从当前播放的项目添加链接
您可以配置当前播放的媒体项目,使其副标题、说明或两者都链接到其他媒体项目。这可以让用户快速跳转到相关项目;例如,他们可能会跳转到同一艺术家的其他歌曲、该播客的其他剧集等。如果汽车支持此功能,用户可以点击链接以浏览到该内容。
要添加链接,请配置KEY_SUBTITLE_LINK_MEDIA_ID
元数据(从副标题链接)或KEY_DESCRIPTION_LINK_MEDIA_ID
(从说明链接)。有关详细信息,请参阅这些元数据字段的参考文档。
支持语音操作
您的媒体应用必须支持语音操作,以帮助为驾驶员提供安全便捷的体验,最大程度地减少干扰。例如,如果您的应用正在播放一个媒体项目,用户可以说“播放 [歌曲标题]”以告诉您的应用播放不同的歌曲,而无需查看或触摸汽车的显示屏。用户可以通过点击方向盘上的相应按钮或说出热词“好的 Google”来发起查询。
当 Android Auto 或 Android Automotive OS 检测并解释语音操作时,该语音操作将通过onPlayFromSearch()
传递给应用。在收到此回调后,应用会查找与query
字符串匹配的内容并开始播放。
用户可以在查询中指定不同类别的术语:流派、艺术家、专辑、歌曲名称、广播电台或播放列表等。在构建搜索支持时,请考虑对您的应用有意义的所有类别。如果 Android Auto 或 Android Automotive OS 检测到给定查询属于某些类别,它会在 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 用户需要打开您的手机应用以解决错误,请在您的消息中向用户提供该信息。例如,您的错误消息可能显示“登录 [您的应用名称]”,而不是“请登录”。