打造車用媒體應用程式

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. 詳閱 Android Automotive OS 應用程式設計規範Android Auto 應用程式設計規範
  3. 請詳閱本節提出的重要詞彙與概念。

重要詞彙與概念

媒體瀏覽器服務
由媒體應用程式實作且符合 MediaBrowserServiceCompat API 的 Android 服務。您的應用程式會使用此項服務公開內容。
媒體瀏覽器
媒體應用程式使用的 API,用於搜尋媒體瀏覽器服務,並顯示其內容。Android Auto 和 Android Automotive OS 使用媒體瀏覽器來尋找應用程式的媒體瀏覽器服務。
媒體項目

媒體瀏覽器會以 MediaItem 物件的樹狀結構來整理內容。一個媒體項目可以附有以下兩種或其中一種標記:

  • Playable (可播放):這個標記代表此項目是內容樹狀結構上的分葉。此項目代表單一音效串流,例如專輯中的一首歌、有聲書的某一章或 Podcast 單集節目。
  • 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"
    ...
/>

如要使用與行動應用程式不同的圖示,請在媒體瀏覽器服務的資訊清單中加入圖示屬性。

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

  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 字串視為不透明權杖。當用戶端應用程式想要瀏覽子選單或播放媒體項目時,系統會傳遞該權杖。您的應用程式負責將權杖與適當的媒體項目建立關聯。

注意:Android Auto 和 Android Automotive OS 對選單中各層級可顯示的媒體項目數量設有嚴格限制。這些限制能盡量減少駕駛人分心,並利用語音指令操作您的應用程式。詳情請參閱「瀏覽內容詳細資料」和「瀏覽檢視畫面」。

以下程式碼片段說明 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 if 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 if 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() 方法。

建構根選單

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

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

  1. 根子項數量的限制:在大多數情況下,此數量上限為 4。這表示無法顯示四個以上的分頁。
  2. 支援的根子項標記:這個值應為 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 整合作業中有所不同的情況。舉例來說,假如您通常會顯示某個根層級的可播放項目,則可能會根據支援的標記提示值,將它嵌入某個根層級可瀏覽項目底下的巢狀結構。

除了根提示外,還必須遵循其他幾項規範,才能確保分頁以最佳方式呈現:

  1. 為每個分頁項目提供單色 (建議白色) 圖示。
  2. 為每個分頁項目提供簡短但有意義的標籤。縮短標籤可以減少字串遭到截斷的機會。

顯示多媒體圖片

媒體項目的圖片必須使用 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 額外套件中加入特定常數,藉此設定媒體項目顯示方式的全域預設值。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);
}

設定個別項目的內容樣式

內容樣式 API 可讓您覆寫所有可瀏覽媒體項目子項和所有媒體項目的預設內容樣式。

如要覆寫某個可瀏覽媒體項目的「子項」預設值,請在該媒體項目的 MediaDescription 中建立額外套件,並新增如上所述的相同提示。DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE 適用於該項目的可播放子項,而 DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE 則適用於該項目的可瀏覽子項。

如要覆寫特定媒體項目「本身」(非其子項) 的預設值,請在該媒體項目的 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 會讀取媒體工作階段的中繼資料,並尋找特定常數來決定要顯示哪些指標。

圖 2. 播放檢視畫面內含識別歌曲和演出者的中繼資料,以及指出明確內容的圖示

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

下列常數可用於 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());

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

您的應用程式可以在使用者啟動搜尋查詢時,顯示相關內容搜尋結果。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
}

啟用播放控制項

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 to handle play control requests, which
        // implements MediaSession.Callback
        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 to handle play control requests, which
    // implements MediaSession.Callback
    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 會自動將音訊內容傳送至車輛的系統,以便透過車輛音響播放。

如要進一步瞭解播放音訊內容,請參閱「媒體播放」、「管理音訊播放」和「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 方法。

自訂動作的圖示

您建立的每項自訂動作都必須具備圖示資源。車用應用程式可以在多種螢幕尺寸以不同密度執行,因此您提供的圖示必須是向量可繪項目。向量可繪項目可讓您擴充素材資源,而不會失去細節。向量可繪項目也可透過較小的解析度輕鬆將邊緣和邊角對齊像素邊界。

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

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

如果目前的情境無法使用自訂動作,請將自訂動作圖示替換為替代圖示,藉此說明該動作已停用。

圖 5.各種樣式的自訂動作圖示範例

支援語音動作

您的媒體應用程式必須支援語音指令,為駕駛人提供安全便利的體驗,盡可能減少干擾元素。舉例來說,如果應用程式已在播放某個媒體項目,使用者可能會說「播放 [曲目標題]」(例如「播放《波西米亞狂想曲》」) 來指示應用程式播放其他歌曲,不必查看或輕觸車輛的螢幕。使用者只要在方向盤上按下適當的按鈕,或說出啟動字詞 (「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 as 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 as you normally would
    mediaSession.setMetadata(builder.build());
}

處理一般錯誤

應用程式發生錯誤時,您應將播放狀態設為 STATE_ERROR,並使用 setErrorMessage() 方法提供錯誤訊息。錯誤訊息必須提供給使用者,並以使用者目前的語言代碼並經本地化後向使用者顯示。然後,Android Auto 和 Android Automotive OS 會向使用者顯示錯誤訊息。

如要進一步瞭解錯誤狀態,請參閱「處理媒體工作階段:狀態和錯誤」。

如果 Android Auto 使用者必須開啟手機應用程式才能解決錯誤,則您應透過訊息向使用者提供這項資訊。舉例來說,錯誤訊息會顯示「登入 [您的應用程式名稱]」而不是「請登入」。

其他資源