Android Auto 和 Android Automotive OS 有助於將媒體應用程式內容提供給坐在車裡的使用者。車用媒體應用程式必須提供媒體瀏覽器服務,Android Auto、Android Automotive OS 或其他具備媒體瀏覽器的應用程式,才能找到並顯示您的內容。
本指南假設您已經有媒體應用程式,可以在手機上播放音訊,且符合 Android 媒體應用程式架構。
本指南說明了 MediaBrowserService
和 MediaSession
這兩種必要元件。您的應用程式需要使用這些元件,才能在 Android Auto 或 Android Automotive OS 上執行。完成核心媒體基礎架構後,就可以在媒體應用程式加入必要元件,以便支援 Android Auto 和 Android Automotive OS。
事前準備
- 請參閱 Android 媒體 API 說明文件。
- 如需設計指南,請參閱有關建立媒體應用程式的說明文章。
- 請參閱本節列出的重要詞彙與概念。
重要詞彙與概念
- 媒體瀏覽器服務
- 由媒體應用程式實作且符合
MediaBrowserServiceCompat
API 的 Android 服務。您的應用程式會使用此項服務公開內容。 - 媒體瀏覽器
- 媒體應用程式使用的 API,用於搜尋媒體瀏覽器服務,並顯示其內容。Android Auto 和 Android Automotive OS 使用媒體瀏覽器來尋找應用程式的媒體瀏覽器服務。
- 媒體項目
媒體瀏覽器會以
MediaItem
物件的樹狀結構來整理內容。一個媒體項目可以附有以下兩種或其中一種標記:FLAG_PLAYABLE
︰代表此項目是內容樹狀結構上的分葉。此項目代表單一音效串流,例如專輯中的一首歌、有聲書的某一章或 Podcast 單集節目。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 中代表您的應用程式,例如在啟動器和圖示匣上。您可以使用下列資訊清單宣告,指明您想要使用行動應用程式中的圖示,來代表您的汽車媒體應用程式:
<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()
方法時,您的服務必須一律傳回非空值的 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 對根選單結構設有特定限制。這些限制會透過根提示傳遞至 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 必須解析為應用程式資源中的點陣圖或向量可繪項目。針對內容階層中代表項目的 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()
的實作以及 Android 通用音樂播放器範例應用程式中的相關方法。
建立與網路 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
extras 套件中加入特定常數即可。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); }
設定個別項目的內容樣式
透過 Content Style 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*/); }
您的應用程式必須將您想要組合的所有媒體項目,作為一個連續區塊傳遞。舉例來說,假設您想要依序顯示「歌曲」和「專輯」這兩個媒體項目群組,且您的應用程式按照下列順序傳入五個媒體項目:
- 包含
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
的媒體項目 A - 包含
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
的媒體項目 B - 包含
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, "Albums")
的媒體項目 E
由於「歌曲」群組和「專輯」群組的媒體項目沒有透過連續區塊傳遞,因此 Android Auto 和 Android Automotive OS 會將此解讀為下列四個群組:
- 群組 1 稱為「歌曲」,內含媒體項目 A
- 群組 2 稱為「專輯」,內含媒體項目 B
- 群組 3 稱為「歌曲」,內含媒體項目 C 和 D
- 群組 4 稱為「專輯」,內含媒體項目 E
如要讓這兩個群組顯示這些項目,應用程式必須按照下列順序傳遞媒體項目:
- 包含
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
的媒體項目 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, "Albums")
的媒體項目 B - 包含
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
的媒體項目 E
顯示其他中繼資料指標
您可以加入其他中繼資料指標,藉此在媒體瀏覽器樹狀結構和播放期間提供一目瞭然的內容資訊。在瀏覽樹狀結構中,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
:表示長篇內容 (例如 Podcast 劇集或有聲書) 的完成狀態。請使用這個常數做為索引鍵;下列整數常數為可能的值: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 (含) 之間的兩倍。此額外項目進一步提供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 (專屬字串 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
額外項目Bundle
中,使用BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST
鍵,將動作清單新增為Parcelable
Arraylist
。
- 在
- 將動作新增至
MediaItem
物件:- 您可以使用鍵
DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST
,在MediaDescriptionCompat
額外項目中加入動作 ID 清單,藉此將動作新增至個別MediaItem
物件。這份清單必須是您在BrowseRoot
中定義的全域動作清單子集。
- 您可以使用鍵
- 處理動作及傳回進度或結果:
- 在
onCustomAction
中,根據動作 ID 和任何其他所需資料處理動作。您可以使用EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID
鍵從額外項目取得觸發動作的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
以反映動作的新狀態。這樣一來,您就能向使用者提供即時意見回饋,說明他們的動作進度和結果。
範例:下載動作
以下範例說明如何使用這項功能,實作具有三種狀態的下載動作:
- 下載:這是動作的初始狀態。當使用者選取這項動作時,您可以將其替換為「Downloading」,並呼叫
sendProgressUpdate
來更新 UI。 - Downloading:這個狀態表示下載作業正在進行中。您可以使用這個狀態向使用者顯示進度列或其他指標。
- 已下載:這個狀態表示下載已完成。下載完成後,您可以將「下載中」換成「已下載」,並使用
EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
鍵呼叫sendResult
,表示應重新整理項目。此外,您也可以使用EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE
鍵向使用者顯示成功訊息。
這個方法可讓您向使用者提供有關下載程序和目前狀態的明確意見回饋。您還可以利用圖示加入更多詳細資料,顯示 25%、50%、75% 的下載狀態。
範例:收藏的動作
另一個範例是具有兩種狀態的收藏動作:
- 收藏:系統會針對未列入使用者收藏清單的項目顯示這項動作。當使用者選取這項動作時,您可以將其與「Favorited」互換,並使用
EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM
鍵呼叫sendResult
來更新 UI。 - 已加入收藏:系統會針對使用者收藏清單中的項目顯示這項動作。使用者選取這個動作時,您可以將該動作切換為「Favorite」,然後使用
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 會在 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
做為其值。
設定初始播放狀態
當 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());
如需此方法的詳細範例,請參閱 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
方法。
自訂動作的圖示
您建立的每項自訂動作都必須具備圖示資源。車用應用程式可以在多種螢幕尺寸以不同密度執行,因此您提供的圖示必須是向量可繪項目。向量可繪項目可以在縮放大小時不失去細節,還能輕鬆將邊緣與邊角對齊像素邊界,即使在解析度較低的情況下也沒問題。
如果自訂動作會記錄狀態 (例如切換開啟或關閉播放設定),請為不同狀態提供不同的圖示,讓使用者可在選取動作時看到狀態變更。
為已停用的動作提供替代圖示樣式
如果目前的情境無法使用自訂動作,請將自訂動作圖示換成標示動作已停用的替代圖示。
指出音訊格式
如要指出目前播放的媒體使用特殊的音訊格式,您可以指定在支援這項功能的車上顯示相應圖示。您可以在當前播放媒體項目 (傳遞至 MediaSession.setMetadata()
) 的 extras 套件中設定 KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI
和 KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI
。為了配合不同版面配置,請務必設定所有 extras。
此外,您可以設定 KEY_IMMERSIVE_AUDIO
extra,向車輛原始設備製造商 (OEM) 告知這是沉浸式音訊。如此一來,OEM 就能判斷套用音效是否會對沉浸式內容造成干擾,據此謹慎做出決定。
從目前播放的項目新增連結
您可以設定目前正在播放的媒體項目,將其副標題和/或說明設為其他媒體項目的連結。這麼做可讓使用者快速跳到相關項目;例如,使用者可以跳到同一藝人的其他歌曲、該 Podcast 的其他集數等等。如果汽車支援這項功能,使用者就能輕觸連結來瀏覽相關內容。
如要新增連結,請設定 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
參數附加額外項目。系統可能會傳送下列額外內容:
計算空白 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 使用者必須開啟手機應用程式才能解決錯誤,則透過訊息向使用者提供這項資訊。舉例來說,錯誤訊息會顯示「登入 [您的應用程式名稱]」而不是「請登入」。