了解车载应用库基础知识

1. 开始之前

在本 Codelab 中,您将学习如何使用车载 Android 应用库,为 Android AutoAndroid Automotive OS 构建针对驾驶优化的应用。您首先要添加对 Android Auto 的支持,然后通过最少额外工作,创建可在 Android Automotive OS 上运行的应用变体。让应用在这两个平台运行后,您将构建一个附加屏幕和一些基本交互功能!

这不是

  • 关于如何为 Android Auto 和 Android Automotive OS 创建媒体(音频)应用的指南。如需了解如何构建此类应用,请参阅为车辆构建媒体应用
  • 关于如何为 Android Auto 创建消息应用的指南。如需了解如何构建此类应用,请参阅为 Android Auto 构建消息应用

您需要

您将构建

Android Auto

Android Automotive OS

A screen recording showing the app running on Android Auto using the Desktop Head Unit.

A screen recording showing the app running on an Android Automotive OS emulator.

您将学习

  • 车载应用库的客户端-主机架构如何运作。
  • 如何编写您自己的 CarAppServiceSessionScreen 类。
  • 如何在 Android Auto 和 Android Automotive OS 上共享您的实现。
  • 如何使用桌面主控单元 (DHU) 在开发机器上运行 Android Auto。
  • 如何运行 Android Automotive OS 模拟器。

2. 设置

获取代码

  1. 本 Codelab 的代码位于 car-codelabs GitHub 代码库中的 car-app-library-fundamentals 目录中。要克隆它,请运行以下命令
git clone https://github.com/android/car-codelabs.git
  1. 或者,您也可以将代码库下载为 ZIP 文件

打开项目

  • 启动 Android Studio 后,导入项目,仅选择 car-app-library-fundamentals/start 目录。car-app-library-fundamentals/end 目录包含解决方案代码,您可以在任何时候遇到困难或只想查看完整项目时进行参考。

熟悉代码

  • 在 Android Studio 中打开项目后,花些时间查看起始代码。

请注意,应用起始代码分为两个模块::app:common:data

The :app module depends on the :common:data module.

:app 模块包含移动应用的 UI 和逻辑,而 :common:data 模块包含 Place 模型数据类和用于读取 Place 模型的代码库。为简单起见,该代码库从硬编码列表读取数据,但在实际应用中可以轻松地从数据库或后端服务器读取数据。

:app 模块包含对 :common:data 模块的依赖项,以便它可以读取并显示 Place 模型列表。

3. 了解车载 Android 应用库

车载 Android 应用库是一组 Jetpack 库,使开发者能够构建用于车辆的应用。它提供了一个模板化框架,可提供针对驾驶优化的用户界面,同时还能适应车辆中存在的各种硬件配置(例如,输入法、屏幕尺寸和宽高比)。总而言之,这让您作为开发者可以轻松构建应用,并确信它将在运行 Android Auto 和 Android Automotive OS 的各种车辆上良好运行。

了解其工作原理

使用车载应用库构建的应用不会直接在 Android Auto 或 Android Automotive OS 上运行。相反,它们依赖于一个主机应用,该主机应用与客户端应用通信并代表它们渲染客户端的用户界面。Android Auto 本身就是一个主机,而 Google Automotive 应用主机是带有 Google 内置服务的 Android Automotive OS 车辆上使用的主机。以下是您在构建应用时必须扩展的关键车载应用库类

CarAppService

CarAppService 是 Android Service 类的子类,是主机应用与客户端应用(例如您在本 Codelab 中构建的应用)通信的入口点。其主要目的是创建主机应用与之交互的 Session 实例。

Session

您可以将 Session 视为在车辆显示屏上运行的客户端应用实例。与其他 Android 组件一样,它具有自身的生命周期,可用于初始化和拆解 Session 实例整个生命周期中使用的资源。CarAppServiceSession 之间存在一对多关系。例如,一个 CarAppService 可能有两个 Session 实例,一个用于主显示屏,另一个用于仪表盘显示屏,适用于支持仪表盘屏幕的导航应用。

Screen

Screen 实例负责生成由主机应用渲染的用户界面。这些用户界面由 Template 类表示,每个类都建模特定类型的布局,例如网格列表。每个 Session 都管理一个 Screen 实例堆栈,用于处理用户在应用不同部分之间的流程。与 Session 一样,Screen 具有自身的生命周期,您可以接入其中。

A diagram of how the Car App Library works. On the left side are two boxes titled Display. In the center, there is a box titled Host. On the right, there is a box titled CarAppService. Within the CarAppService box, there are two boxes, each titled Session. Within the first Session, there are three Screen boxes on top of each other. Within the second Session, there are two Screen boxes on top of each other. There are arrows between the each of the Displays and the host as well as between the host and the Sessions to indicate how the host manages communication between all of the different components.

您将在本 Codelab 的编写您的 CarAppService 部分编写 CarAppServiceSessionScreen,因此如果尚未完全理解,请不必担心。

4. 设置初始配置

首先,设置包含 CarAppService 的模块并声明其依赖项。

创建车载应用服务模块

  1. 在“项目”窗口中选择 :common 模块,右键点击并选择“新建”>“模块”选项。
  2. 在打开的模块向导中,从左侧列表中选择 Android 库模板(以便此模块可被其他模块用作依赖项),然后使用以下值
  • 模块名称::common:car-app-service
  • 软件包名称:com.example.places.carappservice
  • 最低 SDK:API 23: Android 6.0 (Marshmallow)

Create New Module wizard with the values set as described in this step.

设置依赖项

  1. 在项目级 build.gradle 文件中,添加车载应用库版本的变量声明,如下所示。这使您可以在应用中每个模块上轻松使用相同版本。

build.gradle(项目:Places)

buildscript {
    ext {
        // All versions can be found at https://developer.android.com/jetpack/androidx/releases/car-app
        car_app_library_version = '1.3.0-rc01'
        ...
    }
}
  1. 接下来,向 :common:car-app-service 模块的 build.gradle 文件添加两个依赖项。
  • androidx.car.app:app。这是车载应用库的主要工件,包含构建应用时使用的所有核心类。组成该库的另外三个工件是:androidx.car.app:app-projected 用于 Android Auto 专用功能,androidx.car.app:app-automotive 用于 Android Automotive OS 功能代码,以及 androidx.car.app:app-testing 用于一些对单元测试有用的辅助程序。您将在本 Codelab 的稍后部分使用 app-projectedapp-automotive
  • :common:data。这是现有移动应用使用的相同数据模块,允许每个版本的应用体验使用相同的数据源。

build.gradle(模块::common:car-app-service)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    implementation project(":common:data")
    ...
}

进行此更改后,应用自身模块的依赖关系图如下

The :app and :common:car-app-service modules both depend on the :common:data module.

现在依赖项已设置完毕,是时候编写 CarAppService 了!

5. 编写您的 CarAppService

  1. 首先,在 :common:car-app-service 模块内的 carappservice 软件包中创建一个名为 PlacesCarAppService.kt 的文件。
  2. 在该文件中,创建一个名为 PlacesCarAppService 的类,它扩展了 CarAppService

PlacesCarAppService.kt

class PlacesCarAppService : CarAppService() {

    override fun createHostValidator(): HostValidator {
        return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
    }

    override fun onCreateSession(): Session {
        // PlacesSession will be an unresolved reference until the next step
        return PlacesSession()
    }
}

CarAppService 抽象类为您实现了 Service 方法,例如 onBindonUnbind,并阻止进一步重写这些方法,以确保与主机应用实现适当的互操作性。您只需实现 createHostValidatoronCreateSession 即可。

createHostValidator 返回的 HostValidator 会在您的 CarAppService 绑定时被引用,以确保主机受信任,如果主机与您定义的参数不匹配,则绑定失败。就本 Codelab(以及一般测试)而言,ALLOW_ALL_HOSTS_VALIDATOR 可确保您的应用轻松连接,但不应用于生产环境。如需了解如何为生产应用配置此功能,请参阅 createHostValidator 文档。

对于像这个应用一样简单的应用,onCreateSession 可以简单地返回一个 Session 实例。在更复杂的应用中,这将是初始化长期资源(例如在您的应用在车辆上运行时使用的指标和日志客户端)的好地方。

  1. 最后,您需要在 :common:car-app-service 模块的 AndroidManifest.xml 文件中添加与 PlacesCarAppService 对应的 <service> 元素,以便操作系统(以及通过扩展,其他应用(如主机))知道它的存在。

AndroidManifest.xml(:common:car-app-service)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!--
        This AndroidManifest.xml will contain all of the elements that should be shared across the
        Android Auto and Automotive OS versions of the app, such as the CarAppService <service> element
    -->

    <application>
        <service
            android:name="com.example.places.carappservice.PlacesCarAppService"
            android:exported="true">
            <intent-filter>
                <action android:name="androidx.car.app.CarAppService" />
                <category android:name="androidx.car.app.category.POI" />
            </intent-filter>
        </service>
    </application>
</manifest>

此处有两点需要注意

  • <action> 元素允许主机(和启动器)应用找到该应用。
  • <category> 元素声明了应用类别,这决定了它必须满足哪些应用质量标准(稍后将提供更多详细信息)。其他可能的值包括 androidx.car.app.category.NAVIGATIONandroidx.car.app.category.IOT

创建 PlacesSession 类

  • 创建一个 PlacesSession.kt 文件并添加以下代码

PlacesCarAppService.kt

class PlacesSession : Session() {
    override fun onCreateScreen(intent: Intent): Screen {
        // MainScreen will be an unresolved reference until the next step
        return MainScreen(carContext)
    }
}

对于这样一个简单的应用,您只需在 onCreateScreen 中返回主屏幕即可。然而,由于此方法将 Intent 作为参数,功能更丰富的应用也可能会从中读取并填充屏幕返回堆栈或使用其他条件逻辑

创建 MainScreen 类

接下来,创建一个名为 screen. 的新软件包。

  1. 右键点击 com.example.places.carappservice 软件包,然后选择“新建”>“软件包”(其完整软件包名称将是 com.example.places.carappservice.screen)。您可以在此处放置应用的所有 Screen 子类。
  2. screen 软件包中,创建一个名为 MainScreen.kt 的文件以包含 MainScreen 类,该类扩展了 Screen。目前,它使用 PaneTemplate 显示简单的“Hello, world!”消息。

MainScreen.kt

class MainScreen(carContext: CarContext) : Screen(carContext) {
    override fun onGetTemplate(): Template {
        val row = Row.Builder()
            .setTitle("Hello, world!")
            .build()
        
        val pane = Pane.Builder()
            .addRow(row)
            .build()

        return PaneTemplate.Builder(pane)
            .setHeaderAction(Action.APP_ICON)
            .build()
    }
}

6. 添加对 Android Auto 的支持

尽管您现在已经实现了使应用正常运行所需的所有逻辑,但在 Android Auto 上运行它之前,还需要设置另外两项配置。

添加对车载应用服务模块的依赖项

:app 模块的 build.gradle 文件中,添加以下内容

build.gradle(模块::app)

dependencies {
    ...
    implementation project(path: ':common:car-app-service')
    ...
}

进行此更改后,应用自身模块的依赖关系图如下

The :app and :common:car-app-service modules both depend on the :common:data module. The :app module also depends on the :common:car-app-service module.

这会将您刚刚在 :common:car-app-service 模块中编写的代码与车载应用库中包含的其他组件(例如提供的权限授予 Activity)捆绑在一起。

声明 com.google.android.gms.car.application 元数据

  1. 右键点击 :common:car-app-service 模块,然后选择“新建”>“Android 资源文件”选项,然后覆盖以下值
  • 文件名:automotive_app_desc.xml
  • 资源类型:XML
  • 根元素:automotiveApp

New Resource File wizard with the values set as described in this step.

  1. 在该文件中,添加以下 <uses> 元素,以声明您的应用使用车载应用库提供的模板。

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="template"/>
</automotiveApp>
  1. :app 模块的 AndroidManifest.xml 文件中,添加以下 <meta-data> 元素,它引用您刚刚创建的 automotive_app_desc.xml 文件。

AndroidManifest.xml(:app)

<application ...>

    <meta-data
        android:name="com.google.android.gms.car.application"
        android:resource="@xml/automotive_app_desc" />

    ...

</application>

Android Auto 会读取此文件,让它知道您的应用支持哪些功能,在此示例中,它使用车载应用库的模板系统。此信息随后用于处理行为,例如将应用添加到 Android Auto 启动器以及通过通知打开它

可选:监听投影变化

有时,您想知道用户的设备是否已连接到车辆。您可以使用 CarConnection API 来实现此目的,该 API 提供一个 LiveData,可让您观察连接状态。

  1. 要使用 CarConnection API,首先在 :app 模块中添加对 androidx.car.app:app 工件的依赖项。

build.gradle(模块::app)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    ...
}
  1. 出于演示目的,您可以创建一个简单的 Composable(如下所示),用于显示当前连接状态。在实际应用中,此状态可能会记录在日志中,用于在投影时禁用手机屏幕上的某些功能,或者用于其他目的。

MainActivity.kt

@Composable
fun ProjectionState(carConnectionType: Int, modifier: Modifier = Modifier) {
    val text = when (carConnectionType) {
        CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not projecting"
        CarConnection.CONNECTION_TYPE_NATIVE -> "Running on Android Automotive OS"
        CarConnection.CONNECTION_TYPE_PROJECTION -> "Projecting"
        else -> "Unknown connection type"
    }

    Text(
        text = text,
        style = MaterialTheme.typography.bodyMedium,
        modifier = modifier
    )
}
  1. 现在,有了显示数据的方法,请按照以下代码段的演示,读取数据并将其传递到 Composable 中。

MainActivity.kt

setContent {
    val carConnectionType by CarConnection(this).type.observeAsState(initial = -1)
    PlacesTheme {
        // A surface container using the 'background' color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            Column {
                Text(
                    text = "Places",
                    style = MaterialTheme.typography.displayLarge,
                    modifier = Modifier.padding(8.dp)
                )
                ProjectionState(
                    carConnectionType = carConnectionType,
                    modifier = Modifier.padding(8.dp)
                )
                PlaceList(places = PlacesRepository().getPlaces())
            }
        }
    }
}
  1. 如果您运行该应用,它应该显示“未投影”

There is now an additional line of text on the screen for the projection state that says 'Not projecting'

7. 使用桌面主控单元 (DHU) 进行测试

CarAppService 已实现且 Android Auto 配置已到位后,是时候运行应用并查看其外观了。

  1. 在手机上安装该应用,然后按照有关安装和运行 DHU 的说明进行操作

DHU 启动并运行后,您应该会在启动器中看到应用图标(如果没有,请仔细检查您是否已按照上一部分中的所有步骤进行操作,然后从终端退出并重新启动 DHU)。

  1. 从启动器打开应用

The Android Auto launcher showing the app grid, including the Places app.

糟糕,它崩溃了!

There is an error screen with the message 'Android Auto has encountered an unexpected error'. There is a debug toggle in the upper right corner of the screen.

  1. 要查看应用崩溃的原因,您可以切换右上角的调试图标(仅在使用 DHU 运行时可见)或检查 Android Studio 中的 Logcat。

The same error screen as the previous figure, but now with the debug toggle enabled. A stack trace is displayed on the screen.

Error: [type: null, cause: null, debug msg: java.lang.IllegalArgumentException: Min API level not declared in manifest (androidx.car.app.minCarApiLevel)
        at androidx.car.app.AppInfo.retrieveMinCarAppApiLevel(AppInfo.java:143)
        at androidx.car.app.AppInfo.create(AppInfo.java:91)
        at androidx.car.app.CarAppService.getAppInfo(CarAppService.java:380)
        at androidx.car.app.CarAppBinder.getAppInfo(CarAppBinder.java:255)
        at androidx.car.app.ICarApp$Stub.onTransact(ICarApp.java:182)
        at android.os.Binder.execTransactInternal(Binder.java:1285)
        at android.os.Binder.execTransact(Binder.java:1244)
]

从日志中,您可以看到清单中缺少对应用支持的最低 API 级别的声明。在添加该条目之前,最好先了解其必要性。

与 Android 本身一样,车载应用库也有 API 级别的概念,因为主机和客户端应用之间需要有一个契约才能进行通信。主机应用支持给定的 API 级别及其相关功能(以及出于向后兼容性考虑,也支持早期级别中的功能)。例如,SignInTemplate 可在运行 API 级别 2 或更高版本的主机上使用。但是,如果您尝试在仅支持 API 级别 1 的主机上使用它,该主机将不知道模板类型,也无法对其进行任何有意义的操作。

在将主机绑定到客户端的过程中,支持的 API 级别必须有一些重叠,以便绑定成功。例如,如果主机仅支持 API 级别 1,但客户端应用没有 API 级别 2 的功能就无法运行(如本清单声明所示),则应用不应该连接,因为客户端无法在主机上成功运行。因此,客户端必须在其清单中声明所需的最低 API 级别,以确保只有支持它的主机才绑定到它。

  1. 要设置最低支持 API 级别,请在 :common:car-app-service 模块的 AndroidManfiest.xml 文件中添加以下 <meta-data> 元素

AndroidManifest.xml(:common:car-app-service)

<application>
    <meta-data
        android:name="androidx.car.app.minCarApiLevel"
        android:value="1" />
    <service android:name="com.example.places.carappservice.PlacesCarAppService" ...>
        ...
    </service>
</application>
  1. 再次安装该应用并在 DHU 上启动它,您应该会看到以下内容

The app shows a basic 'Hello, world' screen

为了完整性起见,您还可以尝试将 minCarApiLevel 设置为一个较大的值(例如 100),以查看当主机和客户端不兼容时尝试启动应用会发生什么(提示:它会崩溃,类似于未设置任何值的情况)。

同样重要的是,与 Android 本身一样,如果您在运行时验证主机是否支持所需的级别,则可以使用高于声明的最低 API 级别的功能。

可选:监听投影变化

  • 如果您在上一步中添加了 CarConnection 监听器,您应该会在 DHU 运行时在手机上看到状态更新,如下所示

The line of text displaying the projection state now says 'Projecting' since the phone is connected to the DHU.

8. 添加对 Android Automotive OS 的支持

Android Auto 已启动并运行,现在是时候付出额外努力,也支持 Android Automotive OS 了。

创建 :automotive 模块

  1. 要创建包含 Android Automotive OS 版本应用特定代码的模块,请在 Android Studio 中打开“文件”>“新建”>“新建模块...”,从左侧模板类型列表中选择“Automotive”选项,然后使用以下值
  • 应用/库名称:Places(与主应用相同,但如果需要,也可以选择不同的名称)
  • 模块名称:automotive
  • 软件包名称:com.example.places.automotive
  • 语言:Kotlin
  • 最低 SDK:API 29: Android 10.0 (Q)——如前所述,在创建 :common:car-app-service 模块时,所有支持车载应用库应用的 Android Automotive OS 车辆都至少运行 API 29。

The Create New Module wizard for the Android Automotive OS module showing the values listed in this step.

  1. 点击“下一步”,然后在下一个屏幕上选择“无 Activity”,最后点击“完成”

The second page of the Create New Module wizard. Three options are shown, 'No Activity', 'Media Service', and 'Messaging Service'. The 'No Activity' option is selected.

添加依赖项

就像 Android Auto 一样,您需要声明对 :common:car-app-service 模块的依赖项。这样,您就可以在两个平台之间共享您的实现!

此外,您还需要添加对 androidx.car.app:app-automotive 工件的依赖项。与 Android Auto 可选的 androidx.car.app:app-projected 工件不同,Android Automotive OS 需要此依赖项,因为它包含用于运行您应用的 CarAppActivity

  1. 要添加依赖项,请打开 build.gradle 文件,然后插入以下代码

build.gradle(模块::automotive)

dependencies {
    ...
    implementation project(':common:car-app-service')
    implementation "androidx.car.app:app-automotive:$car_app_library_version"
    ...
}

进行此更改后,应用自身模块的依赖关系图如下

The :app and :common:car-app-service modules both depend on the :common:data module. The :app and :automotive modules depend on the :common:car-app-service module.

设置清单

  1. 首先,您需要将 android.hardware.type.automotiveandroid.software.car.templates_host 这两个功能声明为必需

android.hardware.type.automotive 是一个系统功能,它指示设备本身是车辆(有关详细信息,请参阅 FEATURE_AUTOMOTIVE)。只有将此功能标记为必需的应用才能提交到 Play 管理中心的 Automotive OS 轨道(提交到其他轨道的应用不能要求此功能)。android.software.car.templates_host 是一个系统功能,仅存在于具有运行模板应用所需模板主机的车辆中。

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />
    <uses-feature
        android:name="android.software.car.templates_host"
        android:required="true" />
    ...
</manifest>
  1. 接下来,您需要将某些功能声明为非必需。

这是为了确保您的应用与内置 Google 服务的汽车中可用的硬件范围兼容。例如,如果您的应用需要 android.hardware.screen.portrait 功能,它将与带有横向屏幕的车辆不兼容,因为大多数车辆的屏幕方向是固定的。因此,这些功能的 android:required 属性设置为 false

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <uses-feature
        android:name="android.hardware.wifi"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.portrait"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.screen.landscape"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    ...
</manifest>
  1. 接下来,您需要添加对 automotive_app_desc.xml 文件的引用,就像您为 Android Auto 所做的那样。

请注意,这次 android:name 属性与之前不同——它不是 com.google.android.gms.car.application,而是 com.android.automotive。与之前一样,这引用了 :common:car-app-service 模块中的 automotive_app_desc.xml 文件,这意味着 Android Auto 和 Android Automotive OS 都使用相同的资源。请注意,<meta-data> 元素位于 <application> 元素内部(因此您必须将 application 标签从自闭合更改为非自闭合)!

AndroidManifest.xml(:automotive)

<application>
    ...
    <meta-data android:name="com.android.automotive"
        android:resource="@xml/automotive_app_desc"/>
    ...
</application>
  1. 最后,您需要为库中包含的 CarAppActivity 添加一个 <activity> 元素。

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <application ...>
        ...
        <activity
            android:name="androidx.car.app.activity.CarAppActivity"
            android:exported="true"
            android:launchMode="singleTask"
            android:theme="@android:style/Theme.DeviceDefault.NoActionBar">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="distractionOptimized"
                android:value="true" />
        </activity>
    </application>
</manifest>

以下是所有这些操作的含义

  • android:name 列出了 app-automotive 软件包中 CarAppActivity 类的完全限定类名。
  • android:exported 设置为 true,因为此 Activity 必须可由应用本身以外的应用(即启动器)启动。
  • android:launchMode 设置为 singleTask,以便 CarAppActivity 只能同时存在一个实例。
  • android:theme 设置为 @android:style/Theme.DeviceDefault.NoActionBar,以便应用占据其可用的全部屏幕空间。
  • Intent 过滤器指示这是应用的启动器 Activity
  • 有一个 <meta-data> 元素,它向系统指示该应用可以在存在 UX 限制(例如车辆行驶时)的情况下使用。

可选:从 :app 模块复制启动器图标

由于您刚刚创建了 :automotive 模块,它具有默认的绿色 Android 徽标图标。

  • 如果需要,您可以将 mipmap 资源目录从 :app 模块复制并粘贴到 :automotive 模块中,以使用与移动应用相同的启动器图标!

9. 使用 Android Automotive OS 模拟器进行测试

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

  1. 首先,在 Android Studio 中打开 SDK 管理器,如果尚未选择,请选择“SDK 平台”标签页。在 SDK 管理器窗口的右下角,确保“显示软件包详情”复选框已选中。
  2. 安装以下一个或多个模拟器映像。映像只能在与自身具有相同架构(x86/ARM)的机器上运行。
  • Android 12L > 带 Play 商店的 Automotive Intel x86 Atom_64 系统映像
  • Android 12L > 带 Play 商店的 Automotive ARM 64 v8a 系统映像
  • Android 11 > 带 Play 商店的 Automotive Intel x86 Atom_64 系统映像
  • Android 10 > 带 Play 商店的 Automotive Intel x86 Atom_64 系统映像

创建 Android Automotive OS Android 虚拟设备

  1. 打开设备管理器在窗口左侧的“类别”列下选择“Automotive”。然后,从列表中选择 Automotive (1024p landscape) 设备定义,然后点击“下一步”

The Virtual Device Configuration wizard showing the 'Automotive (1024p landscape)' hardware profile selected.

  1. 在下一页上,从上一步中选择一个系统映像(如果您选择了 Android 11/API 30 映像,它可能位于“x86 映像”标签页下,而不是默认的“推荐”标签页)。点击“下一步”并选择您想要的任何高级选项,最后点击“完成”创建 AVD。

运行应用

  1. 使用 automotive 运行配置,在您刚刚创建的模拟器上运行该应用。

The

当您首次运行应用时,可能会看到如下屏幕

The app displays a screen saying 'System update required' with a button that says 'Check for updates' below it.

如果是这种情况,请点击“检查更新”按钮,这将带您进入 Google Automotive 应用主机应用的 Play 商店页面,您应该点击“安装”按钮。如果您在点击“检查更新”按钮时未登录,系统会引导您完成登录流程。登录后,您可以再次打开应用,然后点击按钮返回 Play 商店页面。

The Google Automotive App Host Play Store page - there is an 'Install' button in the upper right corner.

  1. 最后,在安装主机后,再次从启动器(底部行中的九点网格图标)打开应用,您应该会看到以下内容

The app shows a basic 'Hello, world' screen

在下一步中,您将在 :common:car-app-service 模块中进行更改,以显示地点列表,并允许用户在另一个应用中启动导航到所选位置。

10. 添加地图和详情屏幕

向主屏幕添加地图

  1. 首先,将 MainScreen 类的 onGetTemplate 方法中的代码替换为以下内容

MainScreen.kt

override fun onGetTemplate(): Template {
    val placesRepository = PlacesRepository()
    val itemListBuilder = ItemList.Builder()
        .setNoItemsMessage("No places to show")

    placesRepository.getPlaces()
        .forEach {
            itemListBuilder.addItem(
                Row.Builder()
                    .setTitle(it.name)
                    // Each item in the list *must* have a DistanceSpan applied to either the title
                    // or one of the its lines of text (to help drivers make decisions)
                    .addText(SpannableString(" ").apply {
                        setSpan(
                            DistanceSpan.create(
                                Distance.create(Math.random() * 100, Distance.UNIT_KILOMETERS)
                            ), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
                        )
                    })
                    .setOnClickListener { TODO() }
                    // Setting Metadata is optional, but is required to automatically show the
                    // item's location on the provided map
                    .setMetadata(
                        Metadata.Builder()
                            .setPlace(Place.Builder(CarLocation.create(it.latitude, it.longitude))
                                // Using the default PlaceMarker indicates that the host should
                                // decide how to style the pins it shows on the map/in the list
                                .setMarker(PlaceMarker.Builder().build())
                                .build())
                            .build()
                    ).build()
            )
        }

    return PlaceListMapTemplate.Builder()
        .setTitle("Places")
        .setItemList(itemListBuilder.build())
        .build()
}

此代码从 PlacesRepository 读取 Place 实例列表,然后将它们中的每一个转换为 Row,以添加到 PlaceListMapTemplate 显示的 ItemList 中。

  1. 再次运行应用(在任一平台或两个平台),查看结果!

Android Auto

Android Automotive OS

Another stack trace is shown due to an error

The app just crashes and the user is taken back to the launcher after opening it.

糟糕,又一个错误——看起来缺少一个权限。

java.lang.SecurityException: The car app does not have a required permission: androidx.car.app.MAP_TEMPLATES
        at android.os.Parcel.createExceptionOrNull(Parcel.java:2373)
        at android.os.Parcel.createException(Parcel.java:2357)
        at android.os.Parcel.readException(Parcel.java:2340)
        at android.os.Parcel.readException(Parcel.java:2282)
        ...
  1. 要修复此错误,请在 :common:car-app-service 模块的清单中添加以下 <uses-permission> 元素。

任何使用 PlaceListMapTemplate 的应用都必须声明此权限,否则应用会像刚刚演示的那样崩溃。请注意,只有将类别声明androidx.car.app.category.POI 的应用才能使用此模板,进而使用此权限。

AndroidManifest.xml(:common:car-app-service)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
    ...
</manifest>

添加权限后,如果您运行应用,它应该在每个平台上显示如下内容

Android Auto

Android Automotive OS

A list of locations is shown on the left side of the screen and a map with pins corresponding to the locations is shown behind it, filling the rest of the screen.

A list of locations is shown on the left side of the screen and a map with pins corresponding to the locations is shown behind it, filling the rest of the screen.

当您提供必要的 Metadata 后,地图渲染将由应用主机为您处理!

添加详情屏幕

接下来,是时候添加一个详情屏幕了,让用户查看有关特定位置的更多信息,并选择使用他们偏好的导航应用导航到该位置,或返回其他地点列表。可以使用 PaneTemplate 来完成此操作,它允许您在可选操作按钮旁边显示最多四行信息。

  1. 首先,右键点击 :common:car-app-service 模块中的 res 目录,然后点击“新建”>“矢量资产”,然后使用以下配置创建导航图标
  • 资产类型:Clip art
  • 剪贴画:navigation
  • 名称:baseline_navigation_24
  • 大小:24dp x 24dp
  • 颜色:#000000
  • 不透明度:100%

Asset Studio wizard showing the inputs mentioned in this step

  1. 然后,在 screen 软件包中,创建一个名为 DetailScreen.kt 的文件(位于现有 MainScreen.kt 文件旁边)并添加以下代码

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {

    override fun onGetTemplate(): Template {
        val place = PlacesRepository().getPlace(placeId)
            ?: return MessageTemplate.Builder("Place not found")
                .setHeaderAction(Action.BACK)
                .build()

        val navigateAction = Action.Builder()
            .setTitle("Navigate")
            .setIcon(
                CarIcon.Builder(
                    IconCompat.createWithResource(
                        carContext,
                        R.drawable.baseline_navigation_24
                    )
                ).build()
            )
            // Only certain intent actions are supported by `startCarApp`. Check its documentation
            // for all of the details. To open another app that can handle navigating to a location
            // you must use the CarContext.ACTION_NAVIGATE action and not Intent.ACTION_VIEW like
            // you might on a phone.
            .setOnClickListener {  carContext.startCarApp(place.toIntent(CarContext.ACTION_NAVIGATE)) }
            .build()

        return PaneTemplate.Builder(
            Pane.Builder()
                .addAction(navigateAction)
                .addRow(
                    Row.Builder()
                        .setTitle("Coordinates")
                        .addText("${place.latitude}, ${place.longitude}")
                        .build()
                ).addRow(
                    Row.Builder()
                        .setTitle("Description")
                        .addText(place.description)
                        .build()
                ).build()
        )
            .setTitle(place.name)
            .setHeaderAction(Action.BACK)
            .build()
    }
}

请特别注意 navigateAction 的构建方式——在其 OnClickListener 中调用 startCarApp 是与 Android Auto 和 Android Automotive OS 上其他应用交互的关键。

现在有两种类型的屏幕,是时候在它们之间添加导航了!车载应用库中的导航使用推拉式堆栈模型,非常适合在驾驶时完成的简单任务流程。

A diagram representation the way in-app navigation works with the Car App Library. On the left, there is a stack with just a MainScreen. Between it and the center stack is an arrow labeled 'Push DetailScreen'. The center stack has a DetailScreen on top of the existing MainScreen. Between the center stack and the right stack there is an arrow labeled 'Pop'. The right stack is the same as the left one, just a MainScreen.

  1. 要从 MainScreen 上的一个列表项导航到该项的 DetailScreen,请添加以下代码

MainScreen.kt

Row.Builder()
    ...
    .setOnClickListener { screenManager.push(DetailScreen(carContext, it.id)) }
    ...

DetailScreen 返回 MainScreen 的导航已处理,因为在构建 DetailScreen 上显示的 PaneTemplate 时调用了 setHeaderAction(Action.BACK)。当用户点击标头操作时,主机将为您处理从堆栈中弹出当前屏幕,但如果需要,此行为可以由您的应用覆盖

  1. 现在运行应用,查看 DetailScreen 和应用内导航的效果!

11. 更新屏幕上的内容

通常,您希望让用户与屏幕交互并更改该屏幕上元素的状态。为了演示如何执行此操作,您将构建功能,让用户可以在 DetailScreen 上切换收藏和取消收藏某个地点。

  1. 首先,添加一个局部变量 isFavorite 来保存状态。在实际应用中,这应该作为数据层的一部分存储,但对于演示目的而言,局部变量就足够了。

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
    private var isFavorite = false
    ...
}
  1. 接下来,右键点击 :common:car-app-service 模块中的 res 目录,然后点击“新建”>“矢量资产”,然后使用以下配置创建收藏图标
  • 资产类型:Clip art
  • 名称:baseline_favorite_24
  • 剪贴画:favorite
  • 大小:24dp x 24dp
  • 颜色:#000000
  • 不透明度:100%

Asset Studio wizard showing the inputs mentioned in this step

  1. 然后,在 DetailsScreen.kt 中,为 PaneTemplate 创建一个 ActionStrip

ActionStrip UI 组件位于标题对面的标题行中,非常适合次要和三级操作。由于导航是 DetailScreen 上要执行的主要操作,因此将收藏或取消收藏的操作放置在 ActionStrip 中是构建屏幕的好方法。

DetailScreen.kt

val navigateAction = ...

val actionStrip = ActionStrip.Builder()
    .addAction(
        Action.Builder()
            .setIcon(
                CarIcon.Builder(
                    IconCompat.createWithResource(
                        carContext,
                        R.drawable.baseline_favorite_24
                    )
                ).setTint(
                    if (isFavorite) CarColor.RED else CarColor.createCustom(
                        Color.LTGRAY,
                        Color.DKGRAY
                    )
                ).build()
            )
            .setOnClickListener {
                isFavorite = !isFavorite
            }.build()
    )
    .build()

...

此处有两点值得关注

  • CarIcon 颜色会根据项的状态而变化。
  • setOnClickListener 用于响应用户输入并切换收藏状态。
  1. 别忘了在 PaneTemplate.Builder 上调用 setActionStrip 才能使用它!

DetailScreen.kt

return PaneTemplate.Builder(...)
    ...
    .setActionStrip(actionStrip)
    .build()
  1. 现在,运行应用,看看会发生什么

The DetailScreen is shown. The user is tapping on the favorite icon, but it isn't changing color as expected.

有意思……看起来点击事件正在发生,但 UI 没有更新。

这是因为车载应用库有一个“刷新”的概念。为了限制驾驶员分心,刷新屏幕内容有某些限制(因显示模板而异),每次刷新都必须由您自己的代码通过调用 Screen 类的 invalidate 方法显式请求。仅仅更新 onGetTemplate 中引用的某些状态不足以更新 UI。

  1. 要解决此问题,请按如下方式更新 OnClickListener

DetailScreen.kt

.setOnClickListener {
    isFavorite = !isFavorite
    // Request that `onGetTemplate` be called again so that updates to the
    // screen's state can be picked up
    invalidate()
}
  1. 再次运行应用,您会看到心形图标的颜色在每次点击时都会更新!

The DetailScreen is shown. The user is tapping on the favorite icon and it is now changing color as expected.

就这样,您有了一个与 Android Auto 和 Android Automotive OS 都良好集成的基本应用!

12. 恭喜

您已成功构建了第一个车载应用库应用。现在是时候将所学知识应用到您自己的应用中了!

如前所述,目前只有使用车载应用库构建的特定类别应用可以提交到 Play 商店。如果您的应用是导航应用、兴趣点 (POI) 应用(如您在本 Codelab 中操作的应用)或物联网 (IOT) 应用,今天就可以开始构建您的应用,并在两个平台上发布到生产环境。

每年都会增加新的应用类别,因此即使您无法立即应用所学知识,也请稍后回来查看,届时可能正是将您的应用扩展到车载设备的合适时机!

尝试以下操作

  • 安装 OEM 模拟器(例如Polestar 2 模拟器),了解 OEM 定制如何改变 Android Automotive OS 上车载应用库应用的外观和风格。请注意,并非所有 OEM 模拟器都支持车载应用库应用。
  • 查看Showcase 示例应用,它演示了车载应用库的完整功能。

延伸阅读

参考文档