建構及測試適用於停車狀態的 Android Automotive OS 應用程式:支援行車期間的音訊播放功能

1. 事前準備

這不是:

  • 如何打造適用於 Android Auto 和 Android Automotive OS 的媒體 (音訊 - 例如音樂、無線電、Podcast) 應用程式相關指南。如要進一步瞭解如何建構這類應用程式,請參閱「打造車用媒體應用程式」一文。

軟硬體需求

建構項目

在本程式碼研究室中,您將瞭解如何在 Road Reels 中新增行車期間的音訊播放支援功能,這是一款同時支援行動裝置和 Android Automotive OS 裝置的既有應用程式。

啟用使用者體驗限制時,起始版本會暫停播放。

啟用使用者體驗限制時,完成版應用程式會繼續播放。

課程內容

  • 如何在 Android Automotive OS 影片應用程式中啟用背景音訊播放功能

2. 做好準備

取得程式碼

  1. 您可以在 car-codelabs GitHub 存放區的 build-a-parked-app 目錄中找到本程式碼研究室的程式碼。如要複製這個存放區,請執行下列指令:
git clone https://github.com/android/car-codelabs.git
  1. 或者,您也可以將存放區下載為 ZIP 檔案:

開啟專案

  • 啟動 Android Studio 後,匯入專案並只選取 build-a-parked-app/end 目錄。build-a-parked-app/audio-while-driving 目錄內含解決方案程式碼;如果遇到困難,或只是想查看完整專案,都可以隨時參考。

熟悉程式碼

  • 在 Android Studio 中開啟專案後,請花點時間瀏覽範例程式碼。

3. 瞭解可在車輛停妥時使用的 Android Automotive OS 應用程式

可在車輛停妥時使用的應用程式成為 Android Automotive OS 支援的部分應用程式類別。截至本文撰寫時為止,這些應用程式包含影片串流應用程式、網頁瀏覽器和遊戲等類別。這些應用程式非常適合在車上使用,因為車輛硬體內建 Google 服務且電動車的普及率不斷提高,駕駛和乘客可在充電期間與這類應用程式互動。

車輛在許多方面都類似於平板電腦和摺疊式裝置等其他大螢幕裝置。配備的觸控螢幕大小、解析度和顯示比例相似,也可能為直向或橫向 (但這類裝置的螢幕方向是固定的,與平板電腦不同)。此外,這類連結裝置隨時可能連上或中斷網路連線。考量上述所有因素後,自然會得出以下結論:已針對大螢幕完成最佳化的應用程式,通常還須投入最低限度的工作量,才能為車輛打造絕佳使用者體驗。

與大螢幕應用程式相似,車用應用程式也有應用程式品質等級

  • 第 3 級 - 可供車輛使用:應用程式與大螢幕相容,且可在車輛停妥後使用。雖然這類應用程式可能沒有任何針對車輛進行最佳化調整的功能,但使用者可以像在其他大螢幕 Android 裝置上一樣使用應用程式。符合上述規定的行動應用程式,可透過「車用行動應用程式」計畫,直接發布到車輛。
  • 第 2 級 - 針對車輛完成最佳化調整:應用程式可在車輛中控台螢幕上提供絕佳體驗。為達成這個目的,應用程式需要一些車輛專屬的工程,根據應用程式的類別,提供可用於行車或停車模式的功能。
  • 第 1 級 - 針對車輛提供差異化設計:應用程式可在各種車輛硬體上運作,並配合行車和停車模式分別提供相應的體驗。對於車輛中的不同螢幕 (例如中控台、儀表板,以及許多高級車會配備的全景螢幕等其他螢幕),應用程式都會提供最佳使用者體驗。

在本程式碼研究室中,您將實作行車期間的音訊播放功能 (這是第 1 級功能),讓應用程式成為「車輛差異化」應用程式。

4. 在 Android Automotive OS 模擬器中執行應用程式

安裝 Automotive with Play Store 系統映像檔

  1. 首先,在 Android Studio 中開啟 SDK Manager,然後選取「SDK Platforms」分頁標籤 (如果尚未選取)。在 SDK Manager 視窗的右下角,確認已勾選「Show package details」方塊。
  2. 安裝「Add generic system images」清單中的任一 API 34 Android Automotive 模擬器映像檔。映像檔只能在與本身架構 (x86/ARM) 相同的機器上執行。

建立 Android Automotive OS Android 虛擬裝置

  1. 開啟裝置管理工具後,選取視窗左側「Category」欄下方的「Automotive」。接著,從清單中選取「Automotive (1408p landscape)」隨附的硬體設定檔,然後點選「Next」

6a32a01404a7729f.png

  1. 在下一頁中,選取上一個步驟中的系統映像檔。按一下「Next」,選取所需進階選項,最後點選「Finish」建立 AVD。

執行應用程式

使用 app 執行設定,在剛建立的模擬器上執行應用程式。前往播放器畫面並模擬行車狀態,測試應用程式的行為。

5. 偵測行車期間的音訊播放支援功能

並非所有車輛都支援在行車期間播放音訊,因此需要偵測目前裝置是否支援這項功能,並據以調整應用程式的行為。您可以使用 androidx.car.app:app 程式庫中的 CarFeatures 類別,執行這項操作。

  1. 新增 androidx.car.app:app 構件的依附元件。

libs.version.toml

[versions]
...
carApp = "1.7.0-rc01"


[libraries]
...
androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" }

build.gradle.kts (Module :app)

implementation(libs.androidx.car.app)
  1. 接著,您可以在 RoadReelsPlayer 中判斷是否支援這項功能,並更新用於計算 shouldPreventPlay 值的邏輯。

RoadReelsPlayer.kt

if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
    ...

    val isBackgroundAudioWhileDrivingSupported = CarFeatures.isFeatureEnabled(
        context,
        CarFeatures.FEATURE_BACKGROUND_AUDIO_WHILE_DRIVING
    )

    shouldPreventPlay = !isBackgroundAudioWhileDrivingSupported &&
            carUxRestrictionsManager.currentCarUxRestrictions.isRequiresDistractionOptimization
    invalidateState()

    carUxRestrictionsManager.registerListener { carUxRestrictions: CarUxRestrictions ->
        shouldPreventPlay = !isBackgroundAudioWhileDrivingSupported &&
            carUxRestrictions.isRequiresDistractionOptimization
        ...
    }
}

變更完成後再次執行應用程式,並模擬行車狀態。請注意,當應用程式的 UI 遭到系統遮蔽時,仍會繼續播放音訊。不過,這樣還不算完成。您還需要再進行一些變更,才能符合所有需求。

6. 支援背景播放功能

目前,應用程式的媒體播放功能是由活動處理。因此,啟用使用者體驗限制並遮蔽該活動後,可能會在短時間內繼續播放媒體,但系統最終會快取應用程式,導致播放作業結束。

如要支援長時間播放,就必須更新應用程式,使用服務來處理播放作業。您可以使用 Media3 MediaSessionService 完成這項操作。

建立 MediaSessionService

  1. 在「Project」視窗中的 com.example.android.cars.roadreels 套件上按一下滑鼠右鍵,依序選取「New」>「Kotlin Class/File」。輸入 PlaybackService 做為檔案名稱,然後點選「Class」類型。
  2. 新增以下的 PlaybackService. 實作內容。如要進一步瞭解 MediaSessionService,請參閱「使用 MediaSessionService 在背景播放」一文。

PlaybackService.kt

import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService

@UnstableApi
class PlaybackService : MediaSessionService() {
    private var mediaSession: MediaSession? = null

    override fun onCreate() {
        super.onCreate()
        val player = RoadReelsPlayer(this)
        mediaSession = MediaSession.Builder(this, player).build()
    }

    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
        super.onDestroy()
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        if (controllerInfo.isTrusted || controllerInfo.packageName == packageName) {
            return mediaSession
        }
        return null
    }
}
  1. 在資訊清單檔案中加入下列 <uses-permission> 元素,使用前景服務處理媒體播放作業。

AndroidManifest.xml

<manifest ...>
    ...
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
    ...
</manifest>
  1. 同時在資訊清單中宣告 PlaybackService

AndroidManifest.xml

<application ...>
    ...
    <service
        android:name=".PlaybackService"
        android:foregroundServiceType="mediaPlayback"
        android:exported="true">
        <intent-filter>
            <action android:name="androidx.media3.session.MediaSessionService"/>
        </intent-filter>
    </service>
</application>

更新 PlayerViewModel 以使用 PlaybackService

  1. PlaybackService 會自行建立並顯示 MediaSession,因此 PlayerViewModel 就不需再建立媒體工作階段。找出並刪除以下這行和所有變數參照:

PlayerViewModel.kt

private var mediaSession: MediaSession? = null
  1. 接下來,將 init 區塊中用來建立 RoadReelsPlayer 例項的區段,替換為以下程式碼,即可使用 MediaController 將應用程式連結至 PlaybackService

PlayerViewModel.kt

import android.content.ComponentName
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.example.android.cars.roadreels.PlaybackService
import com.google.common.util.concurrent.MoreExecutors

...

init {
        viewModelScope.launch {
            ...
        }
        
        val sessionToken = SessionToken(
            application,
            ComponentName(application, PlaybackService::class.java)
        )

        val mediaControllerFuture =
            MediaController.Builder(getApplication(), sessionToken).buildAsync()

        mediaControllerFuture.addListener(
            { _player.update { mediaControllerFuture.get() } },
            MoreExecutors.directExecutor()
        )
    }

再次測試應用程式,當應用程式遭到系統封鎖時,應該會顯示不同的 UI。使用者現在可以在行車期間控制播放。當使用者回到停車狀態時,可以按一下退出按鈕,返回完整的應用程式體驗。

33eb8ff3d4035978.gif

移除生命週期播放變更

由於現已支援背景播放功能,您可以移除 PlayerScreen.kt 中的兩個 LifecycleEventEffect 區塊,讓應用程式在使用者離開時仍可繼續播放作業,包括在前一個畫面顯示播放控制項時。

離開播放器畫面時暫停播放

由於實際播放器 (目前包含在 PlaybackService 中) 在離開播放器畫面時不會釋出,您需要新增呼叫,在使用者離開播放器畫面時暫停播放,以維持先前的行為。具體做法是更新 PlayerViewModelonCleared 方法實作方式:

PlayerViewModel.kt

override fun onCleared() {
    super.onCleared()
    _player.value?.apply {
        pause()
        release()
    }
}

7. 更新資訊清單

最後,請在應用程式的資訊清單中新增下列 <uses-feature> 元素,指出應用程式支援在行車期間播放音訊。

AndroidManifest.xml

<application>
    <uses-feature android:name="com.android.car.background_audio_while_driving" android:required="false">
</application>

8. 恭喜

您已成功在影片應用程式中新增在行車期間播放音訊的支援功能。現在就運用所學,套用到自己的應用程式吧!

其他資訊