打造車用媒體應用程式

Android Auto 和 Android Automotive OS 有助於將媒體應用程式內容提供給坐在車裡的使用者。車用媒體應用程式必須提供媒體瀏覽器服務,Android Auto、Android Automotive OS 或其他具備媒體瀏覽器的應用程式,才能找到並顯示您的內容。

本指南假設您已經有媒體應用程式,可以在手機上播放音訊,且符合 Android 媒體應用程式架構

本指南說明了 MediaBrowserServiceMediaSession 這兩種必要元件。您的應用程式需要使用這些元件,才能在 Android Auto 或 Android Automotive OS 上執行。完成核心媒體基礎架構後,就可以在媒體應用程式加入必要元件,以便支援 Android AutoAndroid Automotive OS

事前準備

  1. 請參閱 Android 媒體 API 說明文件
  2. 如需設計指南,請參閱有關建立媒體應用程式的說明文章。
  3. 請參閱本節列出的重要詞彙與概念。

重要詞彙與概念

媒體瀏覽器服務
由媒體應用程式實作且符合 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>

出處圖示

圖 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 在一般使用者工作流程中與媒體瀏覽器服務的互動方式。

  1. 使用者在 Android Automotive OS 或 Android Auto 上啟動您的應用程式。
  2. Android Automotive OS 或 Android Auto 使用 onCreate() 方法,與應用程式的媒體瀏覽器服務聯絡。實作 onCreate()MediaSessionCompat 方法時,必須建立並註冊 物件及其回呼物件。
  3. Android Automotive OS 或 Android Auto 會呼叫服務的 onGetRoot() 方法,取得內容階層的根媒體項目。系統不會顯示根媒體項目,但會利用這些項目從應用程式中擷取更多內容。
  4. Android Automotive OS 或 Android Auto 會呼叫服務的 onLoadChildren() 方法,取得根媒體項目的子項。Android Automotive OS 和 Android Auto 會將這些媒體項目顯示為頂層內容項目。如要進一步瞭解系統在此層級中的預期項目,請參閱本頁的「建構根選單」。
  5. 如果使用者選取可瀏覽的媒體項目,系統會再次呼叫服務的 onLoadChildren() 方法,以利擷取所選選單項目的子項。
  6. 如果使用者選取可播放的媒體項目,Android Automotive OS 或 Android Auto 會呼叫正確的媒體工作階段回呼方法來執行該項操作。
  7. 如果您的應用程式支援搜尋功能,使用者也可以搜尋您的內容。在此情況下,Android Automotive OS 或 Android Auto 會呼叫服務的 onSearch() 方法。

建立內容階層

Android Auto 和 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() 方法。

建構根選單

圖 2.以導覽分頁的形式呈現根內容。

Android Auto 和 Android Automotive OS 對根選單結構設有特定限制。這些限制會透過根提示傳遞至 MediaBrowserService,而您可以透過傳入 onGetRoot()Bundle 引數讀取這些提示。按照這些提示操作,系統便能以最佳方式將根層級內容顯示為導覽分頁。如未按照這些提示操作,系統可能會捨棄某些根層級內容或使其不易偵測。系統會傳送兩個提示:

請使用以下程式碼來讀取相關根提示:

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_CONTENTContentResolver.SCHEME_ANDROID_RESOURCE 做為本機 URI 傳遞。此本機 URI 必須解析為應用程式資源中的點陣圖或向量可繪項目。針對內容階層中代表項目的 MediaDescriptionCompat 物件,請透過 setIconUri() 傳遞 URI。針對代表目前播放中項目的 MediaMetadataCompat 物件,請使用下列任一索引鍵透過 putString() 傳遞 URI:

下列步驟說明如何從網路 URI 下載圖片,並透過本機 URI 公開。如需更完整的範例,請參閱 openFile()實作以及 Android 通用音樂播放器範例應用程式中的相關方法。

  1. 建立與網路 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();
    }
  2. 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 會讀取此套件,找出這些常數,以判斷適當的樣式。

在套裝組合中可以使用下列額外項目作為索引鍵:

這些索引鍵可對應至下列整數常數值,以影響這些項目的簡報:

下列程式碼片段說明如何將可瀏覽項目的預設內容樣式設為格線,並將可播放項目的預設內容樣式設定為清單:

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*/);
}

您的應用程式必須將您想要組合的所有媒體項目,作為一個連續區塊傳遞。舉例來說,假設您想要依序顯示「歌曲」和「專輯」這兩個媒體項目群組,且您的應用程式按照下列順序傳入五個媒體項目:

  1. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs") 的媒體項目 A
  2. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums") 的媒體項目 B
  3. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs") 的媒體項目 C
  4. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs") 的媒體項目 D
  5. 包含 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

如要讓這兩個群組顯示這些項目,應用程式必須按照下列順序傳遞媒體項目:

  1. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs") 的媒體項目 A
  2. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs") 的媒體項目 C
  3. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs") 的媒體項目 D
  4. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums") 的媒體項目 B
  5. 包含 extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums") 的媒體項目 E

顯示其他中繼資料指標

您可以加入其他中繼資料指標,藉此在媒體瀏覽器樹狀結構和播放期間提供一目瞭然的內容資訊。在瀏覽樹狀結構中,Android Auto 和 Android Automotive OS 會讀取與某個項目相關的額外項目,並尋找特定常數來決定要顯示哪些指標。在媒體播放期間,Android Auto 和 Android Automotive OS 會讀取媒體工作階段的中繼資料,並尋找特定常數來決定要顯示哪些指標。

圖 3.播放檢視畫面含有用於辨識歌曲和演出者的中繼資料,以及標示煽情露骨內容的圖示。

圖 4.在瀏覽檢視畫面中,在第一個項目為尚未播放的內容顯示一個點,並且在第二個項目為已播放部分的內容顯示進度列。

下列常數可用於 MediaItem說明的額外項目和 MediaMetadata 額外項目

下列常數可能用於 MediaItem 說明的額外項目:

如要顯示使用者正在瀏覽媒體樹狀結構時出現的指標,請建立含有一或多個常數的其他套裝組合,並將該套裝組合傳遞至 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 */);

如要顯示目前正在播放的媒體項目指標,您可以在 mediaSessionMediaMetadataCompat 中為 METADATA_KEY_IS_EXPLICITEXTRA_DOWNLOAD_STATUS 宣告 Long 值。播放檢視畫面無法顯示 DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUSDESCRIPTION_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 即時更新進度列,您可以在 MediaMetadataCompatPlaybackStateCompat 中提供其他資訊,將目前播放的內容連結至瀏覽檢視畫面中的媒體項目。媒體項目必須符合下列條件,才能自動更新進度列:

下列程式碼片段說明如何表示目前播放的項目已連結至瀏覽檢視畫面中的特定項目:

Kotlin

import androidx.media.utils.MediaConstants

// When the MediaItem is constructed to show in the browse view.
// Suppose the item was 25% complete when the user launched the browse view.
val mediaItemExtras = Bundle()
mediaItemExtras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25)
val description =
    MediaDescriptionCompat.Builder()
        .setMediaId("my-media-id")
        .setExtras(mediaItemExtras)
        // ...and any other setters.
        .build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

// Elsewhere, when the user has selected MediaItem for playback.
mediaSession.setMetadata(
    MediaMetadataCompat.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id")
        // ...and any other setters.
        .build())

val playbackStateExtras = Bundle()
playbackStateExtras.putString(
    MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id")
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setExtras(playbackStateExtras)
        // ...and any other setters.
        .build())

Java

import androidx.media.utils.MediaConstants;

// When the MediaItem is constructed to show in the browse view.
// Suppose the item was 25% complete when the user launched the browse view.
Bundle mediaItemExtras = new Bundle();
mediaItemExtras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25);
MediaDescriptionCompat description =
    new MediaDescriptionCompat.Builder()
        .setMediaId("my-media-id")
        .setExtras(mediaItemExtras)
        // ...and any other setters.
        .build();
return MediaBrowserCompat.MediaItem(description, /* flags */);

// Elsewhere, when the user has selected MediaItem for playback.
mediaSession.setMetadata(
    new MediaMetadataCompat.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id")
        // ...and any other setters.
        .build());

Bundle playbackStateExtras = new Bundle();
playbackStateExtras.putString(
    MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id");
mediaSession.setPlaybackState(
    new PlaybackStateCompat.Builder()
        .setExtras(playbackStateExtras)
        // ...and any other setters.
        .build());

圖 5.播放檢視畫面內含「搜尋結果」選項,用於查看與使用者語音搜尋相關的媒體項目

您的應用程式可以在使用者啟動搜尋查詢時,顯示相關內容搜尋結果。Android Auto 和 Android Automotive OS 會透過搜尋查詢介面或工作階段中較早進行查詢的預設用途,顯示這些結果。詳情請參閱本指南中的支援語音操作一節。

如要顯示可瀏覽的搜尋結果,請在服務 onGetRoot() 方法的額外套件中加入常數索引鍵 BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED,該索引鍵會對應至布林值 true

下列程式碼片段說明如何在 onGetRoot() 方法中啟用支援:

Kotlin

import androidx.media.utils.MediaConstants

@Nullable
fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putBoolean(
        MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
    return BrowserRoot(ROOT_ID, extras)
}

Java

import androidx.media.utils.MediaConstants;

@Nullable
@Override
public BrowserRoot onGetRoot(
    @NonNull String clientPackageName,
    int clientUid,
    @Nullable Bundle rootHints) {
    Bundle extras = new Bundle();
    extras.putBoolean(
        MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true);
    return new BrowserRoot(ROOT_ID, extras);
}

如要開始提供搜尋結果,請覆寫媒體瀏覽器服務的 onSearch() 方法。每當使用者叫用搜尋查詢介面或「搜尋結果」預設用途時,Android Auto 和 Android Automotive OS 就能將使用者的搜尋字詞轉送至此方法。

您可以使用標題項目將服務的 onSearch() 方法中的搜尋結果分類,讓內容易於瀏覽。舉例來說,如果您的應用程式會播放音樂,您可以按照「專輯」、「演出者」和「歌曲」來編排搜尋結果。

下列程式碼片段顯示 onSearch() 方法的簡易實作方式:

Kotlin

fun onSearch(query: String, extras: Bundle) {
  // Detach from results to unblock the caller (if a search is expensive).
  result.detach()
  object:AsyncTask() {
    internal var searchResponse:ArrayList
    internal var succeeded = false
    protected fun doInBackground(vararg params:Void):Void {
      searchResponse = ArrayList()
      if (doSearch(query, extras, searchResponse))
      {
        succeeded = true
      }
      return null
    }
    protected fun onPostExecute(param:Void) {
      if (succeeded)
      {
        // Sending an empty List informs the caller that there were no results.
        result.sendResult(searchResponse)
      }
      else
      {
        // This invokes onError() on the search callback.
        result.sendResult(null)
      }
      return null
    }
  }.execute()
}
// Populates resultsToFill with search results. Returns true on success or false on error.
private fun doSearch(
    query: String,
    extras: Bundle,
    resultsToFill: ArrayList
): Boolean {
  // Implement this method.
}

Java

@Override
public void onSearch(final String query, final Bundle extras,
                        Result<List<MediaItem>> result) {

  // Detach from results to unblock the caller (if a search is expensive).
  result.detach();

  new AsyncTask<Void, Void, Void>() {
    List<MediaItem> searchResponse;
    boolean succeeded = false;
    @Override
    protected Void doInBackground(Void... params) {
      searchResponse = new ArrayList<MediaItem>();
      if (doSearch(query, extras, searchResponse)) {
        succeeded = true;
      }
      return null;
    }

    @Override
    protected void onPostExecute(Void param) {
      if (succeeded) {
       // Sending an empty List informs the caller that there were no results.
       result.sendResult(searchResponse);
      } else {
        // This invokes onError() on the search callback.
        result.sendResult(null);
      }
    }
  }.execute()
}

/** Populates resultsToFill with search results. Returns true on success or false on error. */
private boolean doSearch(String query, Bundle extras, ArrayList<MediaItem> resultsToFill) {
    // Implement this method.
}

自訂瀏覽動作

單一自訂瀏覽動作。

圖 6. 單一自訂瀏覽動作

自訂瀏覽動作可讓您針對車輛媒體應用程式中的應用程式 MediaItem 物件,新增自訂圖示和標籤,以及處理使用者與這些動作的互動情形。這麼做可讓您以各種方式擴充媒體應用程式的功能,例如新增「下載」、「加入至佇列」、「播放電台」、「收藏」或「移除」動作。

自訂瀏覽動作溢位選單。

圖 7. 自訂瀏覽動作溢位

如果自訂動作的數量超過原始設備製造商 (OEM) 允許顯示的自訂動作數量,系統就會向使用者顯示溢位選單。

運作方式為何?

每個自訂瀏覽動作的定義如下:

  • 動作 ID (專屬字串 ID)
  • 動作標籤 (向使用者顯示的文字)
  • 動作圖示 URI (可著色的向量可繪項目)

您可以全域定義自訂瀏覽動作清單,做為 BrowseRoot 的一部分。接著,您可以將這些動作的子集附加至個別 MediaItem.

使用者與自訂瀏覽動作互動時,應用程式會在 onCustomAction() 中收到回呼。接著,您可以處理動作,並視需要更新 MediaItem 的動作清單。這對有狀態的動作 (例如「收藏」和「下載」) 十分實用。如果是不需要更新的動作 (例如「播放電台」),則不必更新動作清單。

瀏覽節點根目錄中的自訂瀏覽動作。

圖 8. 自訂瀏覽動作工具列

您也可以將自訂瀏覽動作附加至瀏覽節點根層級。這些動作會顯示在主工具列下方的次要工具列中。

如何實作自訂瀏覽動作

以下是將自訂瀏覽動作新增至專案的步驟:

  1. 覆寫 MediaBrowserServiceCompat 實作項目中的兩個方法:
  2. 在執行階段剖析動作限制:
  3. 建立自訂瀏覽動作的全域清單:
    • 針對每個動作,請使用下列鍵建立 Bundle 物件: * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID:動作 ID * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL:動作標籤 * EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI:動作圖示 URI * 將所有動作 Bundle 物件新增至清單。
  4. 將全域清單加入 BrowseRoot
  5. 將動作新增至 MediaItem 物件:
    • 您可以使用鍵 DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST,在 MediaDescriptionCompat 額外項目中加入動作 ID 清單,藉此將動作新增至個別 MediaItem 物件。這份清單必須是您在 BrowseRoot 中定義的全域動作清單子集。
  6. 處理動作及傳回進度或結果:

以下列出您可以在 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 以反映動作的新狀態。這樣一來,您就能向使用者提供即時意見回饋,說明他們的動作進度和結果。

範例:下載動作

以下範例說明如何使用這項功能,實作具有三種狀態的下載動作:

  1. 下載:這是動作的初始狀態。當使用者選取這項動作時,您可以將其替換為「Downloading」,並呼叫 sendProgressUpdate 來更新 UI。
  2. Downloading:這個狀態表示下載作業正在進行中。您可以使用這個狀態向使用者顯示進度列或其他指標。
  3. 已下載:這個狀態表示下載已完成。下載完成後,您可以將「下載中」換成「已下載」,並使用 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM 鍵呼叫 sendResult,表示應重新整理項目。此外,您也可以使用 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE 鍵向使用者顯示成功訊息。

這個方法可讓您向使用者提供有關下載程序和目前狀態的明確意見回饋。您還可以利用圖示加入更多詳細資料,顯示 25%、50%、75% 的下載狀態。

範例:收藏的動作

另一個範例是具有兩種狀態的收藏動作:

  1. 收藏:系統會針對未列入使用者收藏清單的項目顯示這項動作。當使用者選取這項動作時,您可以將其與「Favorited」互換,並使用 EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM 鍵呼叫 sendResult 來更新 UI。
  2. 已加入收藏:系統會針對使用者收藏清單中的項目顯示這項動作。使用者選取這個動作時,您可以將該動作切換為「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_PREVIOUSACTION_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_NEXTSESSION_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_STOPPEDSTATE_PAUSEDSTATE_NONESTATE_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 方法。

自訂動作的圖示

您建立的每項自訂動作都必須具備圖示資源。車用應用程式可以在多種螢幕尺寸以不同密度執行,因此您提供的圖示必須是向量可繪項目。向量可繪項目可以在縮放大小時不失去細節,還能輕鬆將邊緣與邊角對齊像素邊界,即使在解析度較低的情況下也沒問題。

如果自訂動作會記錄狀態 (例如切換開啟或關閉播放設定),請為不同狀態提供不同的圖示,讓使用者可在選取動作時看到狀態變更。

為已停用的動作提供替代圖示樣式

如果目前的情境無法使用自訂動作,請將自訂動作圖示換成標示動作已停用的替代圖示。

圖 6.顯示自訂動作已停用的圖示範例。

指出音訊格式

如要指出目前播放的媒體使用特殊的音訊格式,您可以指定在支援這項功能的車上顯示相應圖示。您可以在當前播放媒體項目 (傳遞至 MediaSession.setMetadata()) 的 extras 套件中設定 KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URIKEY_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 使用者必須開啟手機應用程式才能解決錯誤,則透過訊息向使用者提供這項資訊。舉例來說,錯誤訊息會顯示「登入 [您的應用程式名稱]」而不是「請登入」。

其他資源