学习汽车应用库基础知识

1. 开始之前

在本 Codelab 中,您将学习如何使用 Android for Cars 应用库Android AutoAndroid Automotive OS 构建旨在减少分心的应用程序。您首先添加对 Android Auto 的支持,然后以最少的工作量创建应用程序的变体,该变体可以在 Android Automotive OS 上运行。在使应用程序在这两个平台上运行后,您将构建一个额外的屏幕和一些基本的交互性!

本 Codelab 不包含的内容

您需要的东西

您将构建的内容

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 上共享您的实现。
  • 如何使用桌面主机单元在开发机器上运行 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 for Cars 应用库

Android for Cars 应用库是 一组 Jetpack 库,使开发人员能够构建可在车辆中使用的应用程序。它提供了一个模板框架,该框架提供驾驶优化的用户界面,同时还处理适应汽车中存在的各种硬件配置(例如,输入方法、屏幕尺寸和纵横比)。总而言之,这使您作为开发人员可以轻松构建应用程序并确信它将在运行 Android Auto 和 Android Automotive OS 的各种车辆上表现良好。

了解其工作原理

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

CarAppService

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

Session

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

屏幕

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 的模块,并声明其依赖项。

创建 car-app-service 模块

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

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

设置 **依赖项**

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

build.gradle(项目:位置)

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 类

  • 创建一个 PlacesCarAppService.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 上运行它之前,还需要设置两部分配置。

添加对 car-app-service 模块的依赖项

: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 模块中编写的代码,以及汽车应用程序库中包含的其他组件,例如 提供的权限授予活动

声明 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 本身一样,Car App Library 也具有 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 模块时,所有支持 Car App Library 应用程序的 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 Console 上的 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. 接下来,您需要像对 Android Auto 一样添加对 automotive_app_desc.xml 文件的引用。

请注意,这次 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,以便应用程序占用可用的全部屏幕空间。
  • 意图过滤器表明这是应用程序的启动器 Activity
  • 有一个 <meta-data> 元素指示系统应用程序可以在存在 UX 限制的情况下使用,例如车辆正在行驶时。

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

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

  • 如果您愿意,可以将 :app 模块中的 mipmap 资源目录复制并粘贴到 :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 App Host 应用程序的 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. 首先,将 MainScreenonGetTemplate 方法中的代码替换为以下代码

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 目录,然后单击 **新建** > **矢量资产**,然后使用以下配置创建导航图标
  • 资产类型: 剪贴画
  • 剪贴画: 导航
  • 名称: 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 上其他应用程序交互的关键。

现在有两种类型的屏幕,是时候添加它们之间的导航了!Car App Library 中的导航使用堆栈模型进行推送和弹出,这对于在驾驶时完成的简单任务流非常理想。

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 目录,然后单击 **新建** > **矢量资产**,然后使用以下配置创建一个收藏图标
  • 资产类型: 剪贴画
  • 名称: baseline_favorite_24
  • 剪贴画: 收藏
  • 尺寸: 24dp x 24dp
  • 颜色: #000000
  • 不透明度: 100%

Asset Studio wizard showing the inputs mentioned in this step

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

ActionStrip UI 组件放置在标题行的标题对面,非常适合次要和三级操作。由于导航是在 DetailScreen 上执行的主要操作,因此将收藏或取消收藏的 Action 放置在 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 并没有更新。

这是因为 Car App Library 有一个 刷新 概念。为了限制驾驶员分心,屏幕上刷新内容有一些限制(根据显示的模板而有所不同),并且每次刷新都必须通过您的代码显式请求,方法是调用 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. 恭喜

您成功构建了第一个 Car App Library 应用程序。现在是时候学习您学到的知识并将其应用到您自己的应用程序中!

如前所述,目前只有使用 Car App Library 应用程序构建的某些类别可以提交到 Play 商店。如果您的应用程序是导航应用程序、兴趣点 (POI) 应用程序(如您在此代码实验室中使用的应用程序)或物联网 (IOT) 应用程序,您现在就可以开始构建并发布您的应用程序,使其在两个平台上都能投入生产。

每年都会添加新的应用程序类别,因此,即使您无法立即应用您学到的知识,也可以稍后回来查看,可能时机已经成熟,可以将您的应用程序扩展到汽车中!

值得尝试的事情

  • 安装 OEM 的模拟器(例如 Polestar 2 模拟器)并查看 OEM 自定义如何改变 Android Automotive OS 上 Car App Library 应用程序的外观和感觉。请注意,并非所有 OEM 模拟器都支持 Car App Library 应用程序。
  • 查看 Showcase 示例应用程序,它展示了 Car App Library 的全部功能。

进一步阅读

参考文档