將現有的 Android 影片應用程式、遊戲和網頁瀏覽器調整為可在 Android Auto 和 Android Automotive OS 上以停車模式運作。請遵循以下步驟,以符合 Google Play 發布的 Car App Quality 指南。
先決條件
- 從一個現有的、支援的停車模式類別(影片串流應用程式、遊戲或網頁瀏覽器)的 Adaptive Android App 開始。
詞彙表
- Parked apps(停車模式應用程式):停車模式應用程式僅在車輛停車時於 Android Auto 和 Android Automotive OS 上執行。三個支援的停車模式類別為影片、遊戲和瀏覽器;其他類別無法作為停車模式應用程式發布。
- Android Auto:Android Auto 是一種由手機驅動的投影系統。應用程式在使用者的手機上執行,並透過有線或無線連線投影到車內顯示器上。Android Auto 是與 Android Automotive OS 不同的平台。
- Android Automotive OS:Android Automotive OS(簡稱 AAOS)是 Android 的一個變體,直接於車輛系統上執行。Android Automotive OS 是與 Android Auto 不同的平台。
- Video category(影片類別):影片應用程式在停車時顯示串流影片。通常在 Android Automotive OS 上可用;Android Auto 上不支援。
- Games category(遊戲類別):遊戲應用程式在停車時提供遊戲體驗。通常在 Android Automotive OS 和 Android Auto 上均可使用。
- Browsers category(瀏覽器類別):網頁瀏覽器讓使用者在停車時瀏覽網頁。在 Android Automotive OS 上以 Beta 版提供;Android Auto 上不支援。
- Media category(媒體類別):非停車模式類別。媒體應用程式在駕駛或停車時播放音訊內容(音樂、廣播、有聲書,不包括影片)。
- Android for Cars platform:Android for Cars 是一個統稱,包含 Android Auto 和 Android Automotive OS。
- Generally available categories(全面上線類別):允許在支援平台上使用內部測試、封閉測試、公開測試和正式發布的 Google Play 軌道。
- Beta categories(Beta 類別):僅允許在內部或封閉測試軌道上使用;公開測試和正式發布軌道則需要先加入搶先體驗計畫。
- Audio while driving(駕駛中音訊):Android Automotive OS 上的 Beta 功能,讓影片應用程式在車輛行駛時可以播放音訊。
- Car app quality guidelines:在依據 Tier 3 Car ready、Tier 2 Car optimized 或 Tier 1 Car differentiated 標準稽核應用程式時,必須檢查 car app quality guidelines。
- Android Automotive OS compatibility mode:相容模式 (
android.software.car.display_compatibility) 是特定車款上的系統功能,提供系統返回操作、安全區域渲染、密度縮放以及不透明的駕駛模式阻擋(將 Activity 移至 Stopped 狀態)。相容模式僅與支援 Android Automotive OS 的應用程式相關;與僅支援 Android Auto 的應用程式無關。
程式碼庫探索
在程式碼庫中搜尋 AndroidManifest.xml(或在 Unity 專案中建立 Assets/Plugins/Android/AndroidManifest.xml)、res/xml/automotive_app_desc.xml、build.gradle、build.gradle.kts 或 gradle/libs.versions.toml 檔案。找到現有的影片播放器、MediaSession 類別、requestedOrientation 呼叫以及 WindowInsets 處理器。除非使用者另有指示,否則在新增依賴項之前請先徵求許可。
步驟 1:配置應用程式 Manifest
在 AndroidManifest.xml 中宣告應用程式類別、目標平台、硬體功能需求和 Activity 配置。
1.1:識別應用程式類別
- Video apps(影片應用程式):影片應用程式必須在
<application>上設定android:appCategory="video",並且不得包含app/src/main/res/xml/automotive_app_desc.xml(其中包含uses name="video")。 - Games(遊戲):遊戲必須在
<application>上設定android:appCategory="game"。如果應用程式支援控制器輸入,請以android:required="false"宣告android.hardware.gamepad。 - Browsers(瀏覽器):瀏覽器必須在已匯出
<activity>(android:exported="true") 的MAIN<intent-filter>中包含<category android:name="android.intent.category.APP_BROWSER"/>。
1.2:識別目標平台和硬體功能
- Android Auto:目標為 Android Auto (Android 15+) 的應用程式必須在
AndroidManifest.xml檔案的<intent-filter>中加入<category android:name="android.intent.category.CAR_LAUNCHER"/>。除非使用者指定了不同的 Intent Filter,否則將其新增到MAIN<intent-filter>中。如果應用程式僅針對 Android Automotive OS,則絕對不要加入此類別。 - Android Automotive OS:目標為 Android Automotive OS 的應用程式必須在
AndroidManifest.xml檔案中宣告<uses-feature android:name="android.hardware.type.automotive" android:required="false"/>(對於行動裝置或多規格軌道為"false";僅當應用程式將在專用 Automotive OS 軌道上發布時才設為"true")。如果應用程式僅針對 Android Auto,則絕對不要加入此功能標籤。 - 非必要硬體功能 (
DO-1):為滿足 Google Play 功能要求和DO-1,請將不支援的功能(android.hardware.wifi、android.hardware.screen.portrait、android.hardware.screen.landscape、android.hardware.camera)宣告為android:required="false",並移除汽車上不存在的功能的android:required="true"設定。 - 顯示連續性 (
LS-C1):在<activity>上配置android:configChanges為screenSize、smallestScreenSize、orientation、screenLayout和density,以便在移動到遠端顯示器或從遠端顯示器移回時,不會重新建立 Activity 或造成播放中斷。 - 相容模式與分心處理:宣告相容模式是可選的,且僅與支援 Android Automotive OS 的應用程式相關。絕對不要為僅支援 Android Auto 的應用程式加入相容模式。加入
android.hardware.type.automotive會停用相容模式,除非在<application>中加入<meta-data android:name="android.software.car.display_compatibility" android:value="true"/>。當應用程式在AndroidManifest.xml中宣告android.software.car.display_compatibility時,應用程式必須在執行階段檢查PackageManager.hasSystemFeature("android.software.car.display_compatibility"),並在該檢查回傳true時隱藏重複的應用程式內返回或關閉操作。停車模式應用程式不得在任何<activity>上包含<meta-data android:name="distractionOptimized" android:value="true"/>(DD-2,DD-3)。
步驟 2:提供返回導航與螢幕方向支援
2.1:加入應用程式內返回與關閉操作
Android Automotive OS 車輛不一定需要配備硬體或軟體的返回按鈕或手勢導航 (AN-1)。請在每個非根目錄畫面(包括詳細資料畫面與全螢幕播放器)提供明確的 UI 返回與關閉控制項:
- 如果
AndroidManifest.xml未宣告android.software.car.display_compatibility:無條件顯示明確的應用程式內返回與關閉控制項。 - 如果
AndroidManifest.xml以android:value="true"宣告android.software.car.display_compatibility:僅在PackageManager.hasSystemFeature("android.software.car.display_compatibility")回傳false時,才顯示應用程式內返回與關閉控制項。
2.2:保護執行階段的螢幕方向請求
車輛顯示器具有固定的方向。在固定為直向的車用顯示器上請求橫向會導致黑邊或閃爍迴圈 (DO-1)。移除在 Manifest 中寫死的 android:screenOrientation 屬性,並在設定 requestedOrientation 之前檢查 PackageManager.FEATURE_SCREEN_LANDSCAPE 和 PackageManager.FEATURE_SCREEN_PORTRAIT。
步驟 3:適應系統列與不規則顯示器開孔
3.1:追蹤可控制的系統列內邊距
在 Android Automotive OS 上,OEM 可以鎖定系統列使其保持可見,以確保空調和車輛控制項始終可存取 (AR-1)。使用 OnControllableInsetsChangedListener 追蹤可控制的內邊距,僅隱藏可控制的系統列,並為不可控制的 statusBars、navigationBars 和 displayCutout 內邊距套用 windowInsetsPadding。
3.2:安全地渲染到不規則顯示器開孔區域
為滿足 Tier 1 的 AR-2 標準(適用於曲面或非矩形車用顯示器),絕對不要使用 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT (0) 或 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES (1)。請使用 car 資源限定詞在 res/values-car-v30/integers.xml 中配置 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS (3) 或 LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER (2):
<integer name="windowLayoutInDisplayCutoutMode">3</integer>
使用 WindowInsets.safeDrawing 保護互動式 UI、應用程式列和可滾動列表。
步驟 4:滿足駕駛分心與媒體播放要求
4.1:在生命週期轉換時暫停播放並阻止駕駛時恢復
當車輛開始移動時,作業系統會用一個阻擋覆蓋層蓋住停車模式的 Activity,並觸發 onPause(在具備相容模式的車輛上則為 onStop)。為滿足 DD-2、DD-3 和 MC-1:
- 使用
androidx.lifecycle:lifecycle-runtime-compose中的LifecycleEventEffect在ON_PAUSE或ON_STOP時暫停播放。 - 暴露一個
MediaSession(androidx.media3:media3-session),包含播放、暫停或停止指令以及媒體項目的中繼資料 (MC-1),並在onCleared中釋放它。 - 在
build.gradle.kts中加入useLibrary("android.car"),將ExoPlayer包裝在ForwardingSimpleBasePlayer中,監聽CarUxRestrictionsManager,當isRequiresDistractionOptimization變為true時暫停播放,並在getState中移除COMMAND_PLAY_PAUSE,使外部的MediaSession控制項無法在駕駛時恢復播放:
@UnstableApi
class CarAwarePlayer(context: Context) :
ForwardingSimpleBasePlayer(ExoPlayer.Builder(context).build()) {
private var shouldPreventPlay = false
private var pausedByUxRestrictions = false
private lateinit var carUxRestrictionsManager: CarUxRestrictionsManager
init {
if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
val car = Car.createCar(context)
carUxRestrictionsManager =
car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE) as CarUxRestrictionsManager
shouldPreventPlay =
carUxRestrictionsManager.currentCarUxRestrictions.isRequiresDistractionOptimization
invalidateState()
carUxRestrictionsManager.registerListener { restrictions: CarUxRestrictions ->
shouldPreventPlay = restrictions.isRequiresDistractionOptimization
if (!shouldPreventPlay && pausedByUxRestrictions) {
handleSetPlayWhenReady(true)
invalidateState()
} else if (shouldPreventPlay && isPlaying) {
pausedByUxRestrictions = true
handleSetPlayWhenReady(false)
invalidateState()
}
}
}
}
override fun getState(): State {
val state = super.getState()
return state.buildUpon()
.setAvailableCommands(
state.availableCommands.buildUpon()
.removeIf(COMMAND_PLAY_PAUSE, shouldPreventPlay)
.build()
)
.build()
}
override fun handleRelease(): ListenableFuture<*> {
if (::carUxRestrictionsManager.isInitialized) {
carUxRestrictionsManager.unregisterListener()
}
return super.handleRelease()
}
}
4.2:支援駕駛中的背景音訊(可選的 Beta 功能)
由於這是選用的 Beta 功能,除非使用者要求,絕對不要實作此功能。若要實作此功能:
- 在
AndroidManifest.xml中宣告<uses-feature android:name="com.android.car.background_audio_while_driving" android:required="false" />,以及FOREGROUND_SERVICE和FOREGROUND_SERVICE_MEDIA_PLAYBACK權限。 - 使用
androidx.car.app:app中的CarFeatures.isFeatureEnabled(context, CarFeatures.FEATURE_BACKGROUND_AUDIO_WHILE_DRIVING)檢查執行階段的車輛支援情況。僅在!isBackgroundAudioWhileDrivingSupported && restrictions.isRequiresDistractionOptimization時才設定shouldPreventPlay。 - 將
MediaSession和播放器託管在一個已匯出的MediaSessionService中,該 Service 需設定android:foregroundServiceType="mediaPlayback",並使用MediaController.Builder和buildAsync從 UI 連接。
步驟 5:保護瀏覽器應用程式中的敏感資料
由於車輛是共享裝置,瀏覽器必須阻止存取已儲存的密碼和付款方式,除非受到使用者設定檔鎖定的保護 (SD-1)。在將密碼或付款資料同步到車輛之前,瀏覽器必須提示使用者進行身分驗證,並在車用螢幕上顯示通知,告知他們的資料將同步到車上 (SD-2)。
使用 KeyguardManager 檢查是否設定了 PIN 碼、圖案或密碼,如果未設定則開啟 Settings.ACTION_SECURITY_SETTINGS,並使用僅限 BiometricManager.Authenticators.DEVICE_CREDENTIAL 的 BiometricPrompt 進行身分驗證(車輛沒有生物辨識硬體):
val keyguardManager = context.getSystemService<KeyguardManager>()
val isDeviceSecure = keyguardManager?.isDeviceSecure == true
lateinit var biometricPrompt: BiometricPrompt
if (!isDeviceSecure) {
context.startActivity(
Intent(Settings.ACTION_SECURITY_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} else {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(context.getString(R.string.auth_title))
.setSubtitle(context.getString(R.string.sync_data_to_car_notice))
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.build()
biometricPrompt.authenticate(promptInfo)
}
步驟 6:驗證實作
使用 ./gradlew assembleDebug 建置應用程式。找出並修正任何建置錯誤。
實作後
…
[內容因翻譯而截斷]