打造車用媒體應用程式

Android Auto 和 Android Automotive OS 有助於將媒體應用程式內容提供給車內使用者。車用媒體應用程式必須提供媒體瀏覽器服務,以便 Android Auto 和 Android Automotive OS (或其他具備媒體瀏覽器的應用程式) 能夠探索及顯示您的內容。

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

本指南說明 MediaBrowserServiceMediaSession 的必要元件,以讓應用程式在 Android Auto 或 Android Automotive OS 執行。完成核心媒體基礎架構後,即可在媒體應用程式中新增對 Android Auto 的支援新增對 Android Automotive OS 的支援

事前準備

  1. 詳閱 Android 媒體 API 說明文件
  2. 詳閱 Android Automotive OS 應用程式設計規範Android Auto 應用程式設計規範
  3. 請詳閱本節提出的重要詞彙與概念。

重要詞彙與概念

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

媒體瀏覽器會以 MediaItem 物件的樹狀結構來整理內容。媒體項目可以包含以下兩個標記之一,或兩者均含:

  • 可播放:此標記代表此項目是內容樹狀結構上的分葉。此項目代表單一音訊串流,例如專輯中的歌曲、有聲書的某一章或 Podcast 單集節目。
  • 可瀏覽:此標記表示此項目是內容樹狀結構中的節點,且具有子項。例如,此項目代表一張專輯,其子項則是專輯中的歌曲。

可瀏覽且可播放的媒體項目,就如同播放清單。您可以選取項目來播放所有子項,或是瀏覽其子項。

車輛最佳化

遵循 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 可在系統使用者介面中使用此圖示代表您的應用程式。

您可以使用下列資訊清單宣告來指定代表應用程式的圖示:

<!--The android:icon attribute is used by Android Automotive OS-->
<application>
    ...
    android:icon="@mipmap/ic_launcher">
    ...
    <!--Used by Android Auto-->
    <meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
               android:resource="@drawable/ic_auto_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. 根子項數量限制:在大部分情況下,此數據可能為四項。這表示無法顯示四個以上的分頁。
  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()

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.各種樣式的自訂動作圖示範例

支援語音動作

您的媒體應用程式必須支援語音動作,為駕駛提供安全便利的體驗,盡可能減少干擾。舉例來說,如果應用程式已開始播放媒體項目,使用者可能會說「播放 [song title]」(例如「播放《波西米亞狂想曲》」) 來指示應用程式,不必查看或輕觸車輛螢幕,即可播放其他歌曲。使用者只要在方向盤上按下適當的按鈕,或說出啟動字詞 (「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 使用者必須開啟手機應用程式才能解決錯誤,則您的訊息需將資訊提供給使用者。舉例來說,錯誤訊息會顯示「登入 [您的應用程式名稱]」而不是「請登入」。

其他資源