构建和测试 Android Automotive OS 驻车应用:支持驾驶时播放音频

1. 开始之前

本文档不介绍

  • 如何为 Android Auto 和 Android Automotive OS 创建媒体(音频,例如音乐、广播、播客)应用的指南。有关如何构建此类应用的详细信息,请参阅构建车载媒体应用

您需要准备什么

您将构建什么

在本 Codelab 中,您将学习如何在现有应用 Road Reels 中添加对驾驶时播放音频的支持,该应用支持移动设备和 Android Automotive OS 设备。

应用的初始版本会在用户体验限制处于活跃状态时暂停播放。

应用的完成版本会在用户体验限制处于活跃状态时继续播放。

您将学到什么

  • 如何在 Android Automotive OS 上的视频应用中启用后台音频播放

2. 设置

获取代码

  1. 本 Codelab 的代码可在 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 层 - 针对汽车差异化:您的应用旨在适用于汽车中的各种硬件,并且可以在驾驶和驻车模式下调整其体验。它为汽车中的不同屏幕(例如中控台、仪表盘和附加屏幕,如许多高端汽车中看到的全景显示屏)提供最佳的用户体验设计。

在本 Codelab 中,您将实现驾驶时播放音频的功能,这是一项第 1 层功能,使该应用成为针对汽车差异化的应用。

4. 在 Android Automotive OS 模拟器中运行应用

安装带有 Play 商店的 Automotive 系统映像

  1. 首先,在 Android Studio 中打开 SDK Manager,如果尚未选择,请选择 SDK Platforms 标签页。在 SDK Manager 窗口的右下角,确保已勾选 Show package details 复选框。
  2. 安装 添加通用系统映像 中列出的某个 API 34 Android Automotive 模拟器映像。映像只能在其自身架构(x86/ARM)相同的机器上运行。

创建 Android Automotive OS Android 虚拟设备

  1. 打开 Device Manager 后,在窗口左侧的 Category 列下选择 Automotive。然后,从列表中选择 Automotive (1408p landscape) 捆绑的硬件配置文件,然后点击 Next

6a32a01404a7729f.png

  1. 在下一页上,选择上一步中的系统映像。点击 Next,然后选择您想要的任何高级选项,最后点击 Finish 创建 AVD。

运行应用

使用 app 运行配置在您刚刚创建的模拟器上运行应用。通过导航到播放器屏幕并模拟驾驶来测试应用的行为。

5. 检测是否支持驾驶时播放音频

由于并非所有车辆都支持驾驶时播放音频,因此您需要检测当前设备是否支持该功能,并相应地调整应用的行为。为此,您可以使用 CarFeatures 类,该类来自 androidx.car.app:app 库。

  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. 支持后台播放

目前,应用的媒体播放由 activity 处理。因此,在用户体验限制生效且 activity 被遮挡后,媒体播放可能会持续一小段时间,但您的应用最终会被系统缓存,导致播放结束。

为了支持长时间播放,必须更新应用以使用 service 处理播放。这可以通过 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. 恭喜!

您已成功向视频应用添加了对驾驶时播放音频的支持。现在,是时候运用您所学的知识并将其应用到您自己的应用中了!

延伸阅读