Communitygithub.com

android/skills

将现有的 Android 视频流应用、游戏和 Web 浏览器适配为 Android Auto 和 Android Automotive OS 上的驻车应用。涵盖清单配置、Google Play 硬件功能声明、兼容模式、返回导航支持、屏幕方向和显示连续性、可控系统栏嵌入、异形屏凹口处理、驾驶分心生命周期管理、使用 CarUxRestrictionsManager 的 MediaSession 播放限制、驾驶时后台音频以及浏览器敏感数据保护。在迁移、构建或审核汽车驻车应用时使用此技能,以满足汽车应用质量标准。

skills 是什么?

本技能指导开发者将现有的 Android 视频流应用、游戏和 Web 浏览器适配为可在 Android Auto 和 Android Automotive OS 上运行的驻车应用。它涵盖了从应用清单配置到运行时行为调整的完整流程,包括:声明应用类别和目标平台、处理硬件功能要求、实现兼容模式、提供返回导航和屏幕方向支持、适应系统栏和异形屏凹口、管理驾驶分心生命周期、使用 CarUxRestrictionsManager 限制 MediaSession 播放(如驾驶时防止恢复播放),以及保护浏览器应用的敏感数据。此技能适用于迁移、构建或审核驻车应用,确保其符合 Google Play 发布的汽车应用质量标准,并明确排除了媒体音频应用、模板化 Car App Library 导航、POI、IoT、天气或通信应用等不适合驾驶时使用的场景。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/android/skills/tree/HEAD/cars/android-for-cars-parked-apps

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

将现有的 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:

  1. 使用 androidx.lifecycle:lifecycle-runtime-compose 中的 LifecycleEventEffect 在 ON_PAUSE 或 ON_STOP 时暂停播放。
  2. 暴露一个包含播放、暂停或停止命令和项目元数据的 MediaSession(androidx.media3:media3-session)(MC-1),并在 onCleared 中释放它。
  3. 在 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 功能,除非用户要求,否则绝不要实现此功能。要实现该功能:

  1. 在 AndroidManifest.xml 中声明 <uses-feature android:name="com.android.car.background_audio_while_driving" android:required="false" />,以及 FOREGROUND_SERVICE 和 FOREGROUND_SERVICE_MEDIA_PLAYBACK 权限。
  2. 使用 androidx.car.app:app 中的 CarFeatures.isFeatureEnabled(context, CarFeatures.FEATURE_BACKGROUND_AUDIO_WHILE_DRIVING) 检查运行时车辆支持。仅当 !isBackgroundAudioWhileDrivingSupported && restrictions.isRequiresDistractionOptimization 时设置 shouldPreventPlay。
  3. 将 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 构建应用。查找并修复所有构建错误。

后实现

……

[截断,待翻译]

相关技能