将现有的 Android 视频应用、游戏和 Web 浏览器适配为 Android Auto 和 Android Automotive OS 上的驻车应用。按照以下步骤操作,以满足通过 Google Play 分发所需的汽车应用质量标准。
前置条件
- 从一个现有的适应型 Android 应用开始,该应用属于支持的驻车应用类别之一:视频流应用、游戏或 Web 浏览器。
术语表
- 驻车应用:驻车应用仅在车辆停车时在 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 不同。
- 视频类别:视频应用在停车时显示流媒体视频。通常在 Android Automotive OS 上可用;在 Android Auto 上不支持。
- 游戏类别:游戏应用在停车时提供游戏体验。通常在 Android Automotive OS 和 Android Auto 上可用。
- 浏览器类别:Web 浏览器允许用户在停车时浏览网页。在 Android Automotive OS 上处于 Beta 阶段;在 Android Auto 上不支持。
- 媒体类别:非驻车类别。媒体应用在驾驶或停车时播放音频内容(音乐、广播、有声读物,不包括视频)。
- Android for Cars 平台:Android for Cars 是一个统称,包含 Android Auto 和 Android Automotive OS。
- 普遍可用类别:允许在受支持平台的内部测试、封闭测试、开放测试和发布版 Google Play 轨道上使用。
- Beta 类别:允许在内部或封闭测试轨道上使用;开放测试和发布版轨道需要加入早期访问计划。
- 驾驶时后台音频:Android Automotive OS 上的一个 Beta 功能,允许视频应用在车辆行驶时播放音频。
- 汽车应用质量标准:在根据 Tier 3(Car ready)、Tier 2(Car optimized)或 Tier 1(Car differentiated)标准审核应用时,必须检查汽车应用质量标准。
- Android Automotive OS 兼容模式:兼容模式(
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:配置应用清单
在 AndroidManifest.xml 中声明应用类别、目标平台、硬件功能要求和 Activity 配置。
1.1:识别应用类别
- 视频应用:视频应用必须在
<application>上设置android:appCategory="video",且不得包含app/src/main/res/xml/automotive_app_desc.xml并设置uses name="video"。 - 游戏:游戏必须在
<application>上设置android:appCategory="game"。如果应用支持控制器输入,请将android.hardware.gamepad声明为android:required="false"。 - 浏览器:浏览器必须在导出的
<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"/>。除非用户指定其他意图过滤器,否则将其添加到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)。移除硬编码的 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)。在 res/values-car-v30/integers.xml 中使用 car 资源限定符配置 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中,并设置android:foregroundServiceType="mediaPlayback",然后使用MediaController.Builder和buildAsync从 UI 连接。
步骤 5:保护浏览器应用中的敏感数据
由于车辆是共享设备,浏览器必须阻止访问保存的密码和支付方式,除非受配置文件锁定保护(SD-1)。在向车辆同步密码或支付数据之前,浏览器必须提示用户进行身份验证,并在汽车屏幕上显示通知,告知用户其数据将同步到汽车(SD-2)。
使用 KeyguardManager 检查是否设置了 PIN、图案或密码,如果未设置则打开 Settings.ACTION_SECURITY_SETTINGS,并使用 BiometricPrompt 进行身份验证,仅使用 BiometricManager.Authenticators.DEVICE_CREDENTIAL(车辆没有生物识别硬件):
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 构建应用。查找并修复所有构建错误。
后实现
……
[截断,待翻译]