使用 Android for Cars 应用库

Android for Cars 应用库可以帮助您实现汽车导航应用、地图注点 (POI) 应用和物联网 (IOT) 应用。为此,它提供了一系列模板,这些模板符合防止驾驶员分心的标准,并且它还解决了一些细节问题,例如存在各种车载显示屏类型和输入模式的问题。

本指南简要介绍了该库的关键功能和概念,并引导您逐步完成设置基本应用的过程。

准备工作

  1. 查看 Design for Driving 中涉及汽车应用库的页面
  2. 查看以下部分列出的关键术语和概念
  3. 熟悉 Android Auto 系统界面Android Automotive OS 设计
  4. 查看版本说明
  5. 查看示例

关键术语和概念

模型和模板
界面由模型对象的图来表示,这些模型对象可以按照它们所属的模板允许的不同方式排列在一起。模板是模型的子集,它们可以在这些图中充当根。模型包含要以文字和图片的形式显示给用户的信息,以及用于配置此类信息的视觉外观各个方面(例如,文字颜色或图片大小)的属性。主机会将模型转换为符合防止驾驶员分心标准的视图,还解决了一些细节问题,例如存在各种车载显示屏类型和输入模式。
主机
主机是一个后端组件,它会实现库的 API 提供的功能,以便您的应用在汽车中运行。从发现应用并管理其生命周期,到将模型转换为视图,再到将用户交互操作通知给应用,这些都属于主机的职责范围。在移动设备上,此主机由 Android Auto 实现。在 Android Automotive OS 上,此主机作为系统应用进行安装。
模板限制
不同的模板会对其模型的内容施加限制。例如,列表模板对可以呈现给用户的项数有限制。模板对可以采用什么方式连接它们以形成任务流也有限制。例如,应用最多只能将 5 个模板推送到屏幕堆栈。如需了解详情,请参阅模板限制
Screen
Screen 是一个由库提供的类,应用实现该类来管理呈现给用户的界面。Screen 具有生命周期,并提供了一种机制,可让应用发送要在屏幕可见时显示的模板。此外,也可以将 Screen 实例推送到 Screen 堆栈以及将从该堆栈中弹出这些实例,这样可以确保它们遵循模板流限制
CarAppService
CarAppService 是一个抽象 Service 类,应用必须实现并导出该类,才能被主机发现并由主机进行管理。应用的 CarAppService 负责使用 createHostValidator 验证主机连接是否可以信任,随后使用 onCreateSession 为每个连接提供 Session 实例。
Session

Session 是一个抽象类,应用必须使用 CarAppService.onCreateSession 实现并返回该类。它充当在车载显示屏上显示信息的入口点,并且具有生命周期,可告知车载显示屏上应用的当前状态,例如当应用可见或隐藏时。

Session 开始时(例如当应用首次启动时),主机会使用 onCreateScreen 方法请求要显示的初始 Screen

安装汽车应用库

有关如何将库添加到应用的说明,请参阅 Jetpack 库发布页面

配置应用的清单文件

您需要先按照以下说明配置应用的清单文件,然后才能创建汽车应用。

声明 CarAppService

主机通过 CarAppService 实现连接到您的应用。您应在清单中声明此服务,以允许主机发现并连接到您的应用。

您还需要在应用的 intent 过滤器的 <category> 元素中声明应用的类别。请查看支持的应用类别的列表,了解此元素允许的值。

以下代码段展示了如何在清单中为地图注点应用声明汽车应用服务:

<application>
    ...
   <service
       ...
        android:name=".MyCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService"/>
        <category android:name="androidx.car.app.category.POI"/>
      </intent-filter>
    </service>

    ...
<application>

支持的应用类别

声明 CarAppService 时,您可以通过在 intent 过滤器中添加以下一个或多个类别值来声明应用的类别,如上一部分中所述:

  • androidx.car.app.category.NAVIGATION:此类应用提供精细导航方向。请参阅构建车载导航应用
  • androidx.car.app.category.POI:此类应用提供与查找停车位、充电站和加油站等地图注点相关的功能。请参阅构建车载地图注点应用
  • androidx.car.app.category.IOT:此类应用可让用户在车内对已连接的设备执行相关操作。请参阅构建车载物联网应用
  • androidx.car.app.category.WEATHER:此类应用可让用户查看与其当前位置或沿途相关的相关天气信息。请参阅构建车载天气应用

如需获取各个类别的详细说明以及有关属于各个类别的应用的标准,请参阅 Android 汽车应用质量

指定应用名称和图标

您需要指定应用名称和图标,主机可以使用它们在系统界面中表示您的应用。

您可以使用 CarAppServicelabelicon 属性来指定用于表示应用的应用名称和图标:

...
<service
   android:name=".MyCarAppService"
   android:exported="true"
   android:label="@string/my_app_name"
   android:icon="@drawable/my_app_icon">
   ...
</service>
...

如果未在 <service> 元素中声明标签或图标,主机将回退到使用为 <application> 元素指定的值。

设置自定义主题

若要为您的汽车应用设置自定义主题,请在清单文件中添加 <meta-data> 元素,如下所示:

<meta-data
    android:name="androidx.car.app.theme"
    android:resource="@style/MyCarAppTheme />

然后,声明样式资源,以便为您的自定义汽车应用主题设置以下属性:

<resources>
  <style name="MyCarAppTheme">
    <item name="carColorPrimary">@layout/my_primary_car_color</item>
    <item name="carColorPrimaryDark">@layout/my_primary_dark_car_color</item>
    <item name="carColorSecondary">@layout/my_secondary_car_color</item>
    <item name="carColorSecondaryDark">@layout/my_secondary_dark_car_color</item>
    <item name="carPermissionActivityLayout">@layout/my_custom_background</item>
  </style>
</resources>

汽车应用 API 级别

汽车应用库定义了自己的 API 级别,以便您了解车辆上的模板主机支持库的哪些功能。如需检索主机支持的最高汽车应用 API 级别,请使用 getCarAppApiLevel() 方法。

AndroidManifest.xml 文件中声明应用支持的最低汽车应用 API 级别:

<manifest ...>
    <application ...>
        <meta-data
            android:name="androidx.car.app.minCarApiLevel"
            android:value="1"/>
    </application>
</manifest>

如需详细了解如何保持向后兼容性以及声明使用某个功能所需的最低 API 级别,请参阅 RequiresCarApi 注释文档。如需了解使用汽车应用库的特定功能所需的 API 级别,请参阅 CarAppApiLevels 参考文档。

创建 CarAppService 和 Session

您的应用需要扩展 CarAppService 类并实现其 onCreateSession 方法,该方法会返回一个 Session 实例,它对应于到主机的当前连接:

Kotlin

class HelloWorldService : CarAppService() {
    ...
    override fun onCreateSession(): Session {
        return HelloWorldSession()
    }
    ...
}

Java

public final class HelloWorldService extends CarAppService {
    ...
    @Override
    @NonNull
    public Session onCreateSession() {
        return new HelloWorldSession();
    }
    ...
}

Session 实例负责返回要在应用首次启动时使用的 Screen 实例:

Kotlin

class HelloWorldSession : Session() {
    ...
    override fun onCreateScreen(intent: Intent): Screen {
        return HelloWorldScreen(carContext)
    }
    ...
}

Java

public final class HelloWorldSession extends Session {
    ...
    @Override
    @NonNull
    public Screen onCreateScreen(@NonNull Intent intent) {
        return new HelloWorldScreen(getCarContext());
    }
    ...
}

若要处理汽车应用需要从应用主屏幕或着陆屏幕以外的屏幕启动的情况(例如处理深层链接),您可以在从 onCreateScreen 返回之前使用 ScreenManager.push 预先植入屏幕的返回堆栈。预先植入可让用户从应用显示的第一个屏幕导航回之前的屏幕。

创建启动屏幕

您可以通过定义扩展 Screen 类的类并实现其 onGetTemplate 方法来创建由应用显示的屏幕,该方法会返回 Template 实例,它表示要在车载显示屏上显示的界面状态。

以下代码段展示了如何声明 Screen,它使用 PaneTemplate 模板显示简单的“Hello World!”字符串:

Kotlin

class HelloWorldScreen(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()
    }
}

Java

public class HelloWorldScreen extends Screen {
    @NonNull
    @Override
    public Template onGetTemplate() {
        Row row = new Row.Builder().setTitle("Hello world!").build();
        Pane pane = new Pane.Builder().addRow(row).build();
        return new PaneTemplate.Builder(pane)
            .setHeaderAction(Action.APP_ICON)
            .build();
    }
}

CarContext 类

CarContext 类是 ContextWrapper 的子类,可供 SessionScreen 实例访问。它提供对汽车服务的访问权限,例如用于管理屏幕堆栈ScreenManager;用于执行常规应用相关功能(例如访问 Surface 对象以绘制地图)的 AppManager;以及用于与主机通信导航元数据和其他导航相关事件NavigationManager

如需查看导航应用可用的库功能的详尽列表,请参阅访问导航模板部分。

CarContext 还提供了一些其他功能,例如允许您使用车载显示屏上的配置加载可绘制资源、使用 intent 在汽车中启动应用,以及指示您的应用是否应以深色主题显示其地图。

实现屏幕导航

应用通常会呈现许多不同的屏幕,每个屏幕可能会利用不同的模板,用户可以在与屏幕中显示的界面交互时浏览这些屏幕。

ScreenManager 类提供了一个屏幕堆栈,您可以使用它来推送屏幕,当用户选择车载显示屏上的返回按钮或使用某些汽车中提供的硬件返回按钮时,可以自动弹出这些屏幕。

以下代码段展示了如何向消息模板添加返回操作,以及在用户选择新屏幕时推入该屏幕的操作:

Kotlin

val template = MessageTemplate.Builder("Hello world!")
    .setHeaderAction(Action.BACK)
    .addAction(
        Action.Builder()
            .setTitle("Next screen")
            .setOnClickListener { screenManager.push(NextScreen(carContext)) }
            .build())
    .build()

Java

MessageTemplate template = new MessageTemplate.Builder("Hello world!")
    .setHeaderAction(Action.BACK)
    .addAction(
        new Action.Builder()
            .setTitle("Next screen")
            .setOnClickListener(
                () -> getScreenManager().push(new NextScreen(getCarContext())))
            .build())
    .build();

Action.BACK 对象是自动调用 ScreenManager.pop 的标准 Action。可通过使用 CarContext 提供的 OnBackPressedDispatcher 实例来替换此行为。

为了帮助确保应用在驾车时可以安全使用,屏幕堆栈的最大深度可以为五个屏幕。如需了解详情,请参阅模板限制部分。

刷新模板的内容

应用可通过调用 Screen.invalidate 方法来请求使 Screen 的内容无效。主机随后回调应用的 Screen.onGetTemplate 方法,以检索包含新内容的模板。

刷新 Screen 时,请务必了解模板中可更新的特定内容,以便主机不会将新模板计入模板配额。如需了解详情,请参阅模板限制部分。

建议您为屏幕设置适当的结构,以使 Screen 与其通过 onGetTemplate 实现返回的模板类型之间存在一对一的映射关系。

绘制地图

使用以下模板的导航应用和地图注点 (POI) 应用可以通过访问 Surface 来绘制地图:

模板 模板权限 类别指南
NavigationTemplate androidx.car.app.NAVIGATION_TEMPLATES 导航
MapWithContentTemplate androidx.car.app.NAVIGATION_TEMPLATES
androidx.car.app.MAP_TEMPLATES
导航地图注点天气
MapTemplate已废弃 androidx.car.app.NAVIGATION_TEMPLATES 导航
PlaceListNavigationTemplate已废弃 androidx.car.app.NAVIGATION_TEMPLATES 导航
RoutePreviewNavigationTemplate已废弃 androidx.car.app.NAVIGATION_TEMPLATES 导航

声明 Surface 权限

除了应用所使用的模板所需的权限之外,应用还必须在其 AndroidManifest.xml 文件中声明 androidx.car.app.ACCESS_SURFACE 权限,才能访问 Surface:

<manifest ...>
  ...
  <uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
  ...
</manifest>

访问 Surface

如需访问主机提供的 Surface,您必须实现 SurfaceCallback 并将该实现提供给 AppManager 汽车服务。当前 Surface 会在 onSurfaceAvailable()onSurfaceDestroyed() 回调的 SurfaceContainer 参数中传递给 SurfaceCallback

Kotlin

carContext.getCarService(AppManager::class.java).setSurfaceCallback(surfaceCallback)

Java

carContext.getCarService(AppManager.class).setSurfaceCallback(surfaceCallback);

了解 Surface 设备的可见区域

主机可以在地图上绘制模板的界面元素。主机将通过调用 SurfaceCallback.onVisibleAreaChanged 来告知保证不被遮挡且完全对用户可见的 Surface 区域。此外,为了最大限度地减少更改次数,主机还会使用根据当前模板将会始终可见的最小矩形来调用 SurfaceCallback.onStableAreaChanged 方法。

例如,如果导航应用使用的是顶部带有操作条NavigationTemplate,当用户有一段时间没有与屏幕交互时,该操作条可能会自动隐藏,以便为地图腾出更多空间。在这种情况下,将使用相同的矩形对 onStableAreaChangedonVisibleAreaChanged 进行回调。当操作栏处于隐藏状态时,仅使用较大的区域调用 onVisibleAreaChanged。如果用户与屏幕交互,则同样仅使用第一个矩形调用 onVisibleAreaChanged

支持深色主题

当主机确定条件允许时,应用必须使用适当的深色将地图重新绘制到 Surface 实例上,如 Android 汽车应用质量中所述。

为了决定是否应绘制深色地图,您可以使用 CarContext.isDarkMode 方法。每当深色主题状态发生变化时,您都会收到对 Session.onCarConfigurationChanged 的调用。

允许用户与您的地图互动

使用以下模板时,您可以添加相应支持来允许用户与您绘制的地图互动,例如让用户通过缩放和平移查看地图的不同部分。

模板 从哪个 Car App API 级别起支持相应互动功能
NavigationTemplate 2
PlaceListNavigationTemplate(已废弃 4
RoutePreviewNavigationTemplate(已废弃 4
MapTemplate(已废弃 5(模板简介)
MapWithContentTemplate 7(模板简介)

实现互动回调

SurfaceCallback 接口提供了多个回调方法,您可以通过实现这些方法,为使用上一部分中的模板构建的地图添加互动性:

互动 SurfaceCallback 方法 从哪个 Car App API 级别开始支持
点按 onClick 5
双指张合即可缩放 onScale 2
单点触控拖动 onScroll 2
单点触控快速滑动 onFling 2
点按两次 onScale(缩放比例由模板主机决定) 2
平移模式下的旋转轻推 onScroll(距离系数由模板主机决定) 2

添加地图操作栏

这些模板可以为地图相关操作(例如放大和缩小、重新居中、显示罗盘或您选择显示的其他操作)提供地图操作栏。地图操作栏最多可包含四个仅显示图标的按钮,这些按钮可在不影响任务深度的情况下刷新。它在空闲状态下会隐藏,在活跃状态下则重新出现。

如需接收地图互动回调,您必须在地图操作栏中添加一个 Action.PAN 按钮。当用户按平移按钮时,主机会进入平移模式,具体如下一部分所述。

如果应用的地图操作栏中没有 Action.PAN 按钮,您将无法从 SurfaceCallback 方法接收用户输入,并且主机会退出先前启用的任何平移模式。

触摸屏上不会显示平移按钮。

了解平移模式

在平移模式下,模板主机会将来自非触控输入设备(例如旋控器和触控板)的用户输入在转换后传递给相应的 SurfaceCallback 方法。系统会使用 NavigationTemplate.Builder 中的 setPanModeListener 方法响应用户操作来进入或退出平移模式。当用户处于平移模式时,主机可以隐藏模板中的其他界面组件。

与用户互动

您的应用可以使用与移动应用类似的模式与用户互动。

处理用户输入

应用可通过将适当的监听器传递给支持它们的模型来响应用户输入。以下代码段展示了如何创建一个 Action 模型,该模型设置了一个 OnClickListener,它会回调由应用代码定义的方法:

Kotlin

val action = Action.Builder()
    .setTitle("Navigate")
    .setOnClickListener(::onClickNavigate)
    .build()

Java

Action action = new Action.Builder()
    .setTitle("Navigate")
    .setOnClickListener(this::onClickNavigate)
    .build();

然后,onClickNavigate 方法可使用 CarContext.startCarApp 方法启动默认的汽车导航应用

Kotlin

private fun onClickNavigate() {
    val intent = Intent(CarContext.ACTION_NAVIGATE, Uri.parse("geo:0,0?q=" + address))
    carContext.startCarApp(intent)
}

Java

private void onClickNavigate() {
    Intent intent = new Intent(CarContext.ACTION_NAVIGATE, Uri.parse("geo:0,0?q=" + address));
    getCarContext().startCarApp(intent);
}

如需详细了解如何启动应用(包括 ACTION_NAVIGATE intent 的格式),请参阅使用 intent 启动汽车应用部分。

某些操作(例如那些需要引导用户在其移动设备上继续交互的操作)只有在汽车停好后才允许执行。您可以使用 ParkedOnlyOnClickListener 实现这些操作。如果汽车没有停好,主机会向用户显示一条消息,指出在这种情况下不允许执行该操作。如果汽车已停好,代码就会正常执行。以下代码段展示了如何使用 ParkedOnlyOnClickListener 在移动设备上打开设置屏幕:

Kotlin

val row = Row.Builder()
    .setTitle("Open Settings")
    .setOnClickListener(ParkedOnlyOnClickListener.create(::openSettingsOnPhone))
    .build()

Java

Row row = new Row.Builder()
    .setTitle("Open Settings")
    .setOnClickListener(ParkedOnlyOnClickListener.create(this::openSettingsOnPhone))
    .build();

显示通知

发送到移动设备的通知只有在使用 CarAppExtender 扩展后才会显示在车载显示屏上。某些通知属性(例如内容标题、文字、图标和操作)可以在 CarAppExtender 中设置,从而在通知显示在车载显示屏上时替换其属性。

以下代码段展示了如何向车载显示屏发送一条通知,让其显示的标题不同于移动设备上显示的标题:

Kotlin

val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    .setContentTitle(titleOnThePhone)
    .extend(
        CarAppExtender.Builder()
            .setContentTitle(titleOnTheCar)
            ...
            .build())
    .build()

Java

Notification notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    .setContentTitle(titleOnThePhone)
    .extend(
        new CarAppExtender.Builder()
            .setContentTitle(titleOnTheCar)
            ...
            .build())
    .build();

通知可能会影响界面的以下几个部分:

  • 可能会向用户显示浮动通知 (HUN)。
  • 可能会在通知中心添加一个条目,并且选择性地在侧边栏显示一个标志。
  • 对于导航应用,通知可能会显示在侧边栏微件中,如精细导航通知中所述。

您可以通过使用通知的优先级,选择如何配置通知以影响每个界面元素,如 CarAppExtender 文档中所述。

如果调用 NotificationCompat.Builder.setOnlyAlertOnce 且将值设置为 true,则高优先级通知将只以 HUN 的形式显示一次。

如需详细了解如何设计汽车应用的通知,请参阅 Google Design for Driving 中有关通知的指南。

显示消息框

应用可以使用 CarToast 显示消息框,如以下代码段所示:

Kotlin

CarToast.makeText(carContext, "Hello!", CarToast.LENGTH_SHORT).show()

Java

CarToast.makeText(getCarContext(), "Hello!", CarToast.LENGTH_SHORT).show();

请求权限

如果您的应用需要访问受限数据或操作(例如位置信息),则应遵循 Android 权限的标准规则。如需请求权限,您可以使用 CarContext.requestPermissions() 方法。

与使用标准 Android API 相比,使用 CarContext.requestPermissions() 的优势在于,您无需启动自己的 Activity 来创建权限对话框。此外,您可以在 Android Auto 和 Android Automotive OS 上使用相同的代码,无需创建依赖于平台的流程。

设置 Android Auto 上的权限对话框的样式

在 Android Auto 上,系统会在手机上向用户显示权限对话框。默认情况下,对话框后面没有背景。若要设置自定义背景,请在 AndroidManifest.xml 文件中声明汽车应用主题,并为汽车应用主题设置 carPermissionActivityLayout 属性。

<meta-data
    android:name="androidx.car.app.theme"
    android:resource="@style/MyCarAppTheme />

然后,为汽车应用主题设置 carPermissionActivityLayout 属性:

<resources>
  <style name="MyCarAppTheme">
    <item name="carPermissionActivityLayout">@layout/my_custom_background</item>
  </style>
</resources>

使用 intent 启动汽车应用

您可以调用 CarContext.startCarApp 方法来执行以下某项操作:

  • 打开拨号器拨打电话。
  • 使用默认汽车导航应用开始精细导航到某个位置。
  • 使用 intent 启动您自己的应用。

以下示例展示了如何创建一条通知,该通知包含一项操作,即打开应用中显示停车预订详情的屏幕。您可以使用内容 intent 扩展通知实例,该 intent 包含 PendingIntent,它将显式 intent 封装到应用的操作中:

Kotlin

val notification = notificationBuilder
    ...
    .extend(
        CarAppExtender.Builder()
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_VIEW_PARKING_RESERVATION.hashCode(),
                    Intent(ACTION_VIEW_PARKING_RESERVATION)
                        .setComponent(ComponentName(context, MyNotificationReceiver::class.java)),
                    0))
            .build())

Java

Notification notification = notificationBuilder
    ...
    .extend(
        new CarAppExtender.Builder()
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_VIEW_PARKING_RESERVATION.hashCode(),
                    new Intent(ACTION_VIEW_PARKING_RESERVATION)
                        .setComponent(new ComponentName(context, MyNotificationReceiver.class)),
                    0))
            .build());

应用还必须声明 BroadcastReceiver,当用户在通知界面中选择相应的操作并使用包含数据 URI 的 intent 调用 CarContext.startCarApp 时,会调用该类来处理 intent:

Kotlin

class MyNotificationReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val intentAction = intent.action
        if (ACTION_VIEW_PARKING_RESERVATION == intentAction) {
            CarContext.startCarApp(
                intent,
                Intent(Intent.ACTION_VIEW)
                    .setComponent(ComponentName(context, MyCarAppService::class.java))
                    .setData(Uri.fromParts(MY_URI_SCHEME, MY_URI_HOST, intentAction)))
        }
    }
}

Java

public class MyNotificationReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String intentAction = intent.getAction();
        if (ACTION_VIEW_PARKING_RESERVATION.equals(intentAction)) {
            CarContext.startCarApp(
                intent,
                new Intent(Intent.ACTION_VIEW)
                    .setComponent(new ComponentName(context, MyCarAppService.class))
                    .setData(Uri.fromParts(MY_URI_SCHEME, MY_URI_HOST, intentAction)));
        }
    }
}

最后,应用中的 Session.onNewIntent 方法通过在堆栈上推入停车预订屏幕(如果还没有在顶部)来处理此 intent:

Kotlin

override fun onNewIntent(intent: Intent) {
    val screenManager = carContext.getCarService(ScreenManager::class.java)
    val uri = intent.data
    if (uri != null
        && MY_URI_SCHEME == uri.scheme
        && MY_URI_HOST == uri.schemeSpecificPart
        && ACTION_VIEW_PARKING_RESERVATION == uri.fragment
    ) {
        val top = screenManager.top
        if (top !is ParkingReservationScreen) {
            screenManager.push(ParkingReservationScreen(carContext))
        }
    }
}

Java

@Override
public void onNewIntent(@NonNull Intent intent) {
    ScreenManager screenManager = getCarContext().getCarService(ScreenManager.class);
    Uri uri = intent.getData();
    if (uri != null
        && MY_URI_SCHEME.equals(uri.getScheme())
        && MY_URI_HOST.equals(uri.getSchemeSpecificPart())
        && ACTION_VIEW_PARKING_RESERVATION.equals(uri.getFragment())
    ) {
        Screen top = screenManager.getTop();
        if (!(top instanceof ParkingReservationScreen)) {
            screenManager.push(new ParkingReservationScreen(getCarContext()));
        }
    }
}

如需详细了解如何处理汽车应用的通知,请参阅显示通知部分。

模板限制

主机将针对给定任务显示的模板数限制为最多 5 个,在这 5 个模板中,最后一个模板必须是以下某种类型:

请注意,此限制适用于模板数,而不是堆栈中的 Screen 实例数。例如,如果在屏幕 A 中,应用发送了 2 个模板,然后推送屏幕 B,那么它现在可以再发送 3 个模板。或者,如果将每个屏幕的结构都设置为发送单个模板,那么应用可以将 5 个屏幕实例推送到 ScreenManager 堆栈上。

这些限制有一些特殊情况:模板刷新、返回和重置操作。

模板刷新

某些内容更新不计入模板限制。一般来说,如果应用推送的新模板所属的类型及其包含的主要内容与之前的模板相同,新模板就不会被计入配额。例如,更新 ListTemplate 中某一行的切换状态不会计入配额。如需详细了解可将哪些类型的内容更新视为刷新,请参阅各个模板的文档。

返回操作

为了在任务中启用子流,主机会检测应用何时从 ScreenManager 堆栈中弹出 Screen,并根据应用倒退的模板数更新剩余配额。

例如,如果在屏幕 A 中,应用发送了 2 个模板,然后推送屏幕 B 并且又发送了 2 个模板,那么应用的剩余配额就为 1。如果应用随后弹回到屏幕 A,主机会将配额重置为 3,因为应用倒退了 2 个模板。

请注意,当弹回到某个屏幕时,应用发送的模板所属的类型必须与该屏幕上次发送的模板的类型相同。发送任何其他类型的模板会导致出现错误。不过,只要类型在返回操作期间保持不变,应用就可以随意修改模板的内容,而不会影响配额。

重置操作

某些模板具有表示任务结束的特殊语义。例如,NavigationTemplate 是一个视图,它应该会持续显示在屏幕上,并使用新的精细导航指示进行刷新,以供用户使用。到达其中一个模板时,主机会重置模板配额,将该模板当作新任务的第一步来对待,从而使应用能够开始新任务。如需了解哪些模板会在主机上触发重置操作,请参阅各个模板的文档。

如果主机收到通过通知操作或从启动器启动应用的 intent,也会重置配额。此机制使应用能够从通知开始新任务流,即使应用已绑定且在前台运行,也是如此。

如需详细了解如何在车载显示屏上显示应用的通知,请参阅显示通知部分。如需了解如何通过通知操作启动应用,请参阅使用 intent 启动汽车应用部分。

Connection API

您可以使用 CarConnection API 在运行时检索连接信息,从而确定您的应用是在 Android Auto 上还是在 Android Automotive OS 上运行。

例如,在您的汽车应用的 Session 中,初始化 CarConnection 并订阅 LiveData 更新:

Kotlin

CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)

Java

new CarConnection(getCarContext()).getType().observe(this, this::onConnectionStateUpdated);

在观察器中,您随后可以对连接状态的变化做出响应:

Kotlin

fun onConnectionStateUpdated(connectionState: Int) {
  val message = when(connectionState) {
    CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
    CarConnection.CONNECTION_TYPE_NATIVE -> "Connected to Android Automotive OS"
    CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto"
    else -> "Unknown car connection type"
  }
  CarToast.makeText(carContext, message, CarToast.LENGTH_SHORT).show()
}

Java

private void onConnectionStateUpdated(int connectionState) {
  String message;
  switch(connectionState) {
    case CarConnection.CONNECTION_TYPE_NOT_CONNECTED:
      message = "Not connected to a head unit";
      break;
    case CarConnection.CONNECTION_TYPE_NATIVE:
      message = "Connected to Android Automotive OS";
      break;
    case CarConnection.CONNECTION_TYPE_PROJECTION:
      message = "Connected to Android Auto";
      break;
    default:
      message = "Unknown car connection type";
      break;
  }
  CarToast.makeText(getCarContext(), message, CarToast.LENGTH_SHORT).show();
}

Constraints API

不同汽车允许每次向用户显示的 Item 实例数量可能会有所不同。使用 ConstraintManager 在运行时查看内容限制并在模板中设置适当的项目数量。

首先,从 CarContext 获取 ConstraintManager

Kotlin

val manager = carContext.getCarService(ConstraintManager::class.java)

Java

ConstraintManager manager = getCarContext().getCarService(ConstraintManager.class);

然后,您可以查询检索到的 ConstraintManager 对象,以了解相关的内容限制。例如,若要获取网格中可显示的项目数量,请使用 CONTENT_LIMIT_TYPE_GRID 调用 getContentLimit

Kotlin

val gridItemLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID)

Java

int gridItemLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID);

添加登录流程

如果您的应用为用户提供登录体验,您可以配合使用 SignInTemplateLongMessageTemplate 等模板以及汽车应用 API 2 及更高级别,以处理在汽车的车机上登录您应用的操作。

若要创建 SignInTemplate,请定义 SignInMethod。汽车应用库目前支持以下登录方法:

举例来说,如需实现收集用户密码的模板,请首先创建 InputCallback 来处理和验证用户输入:

Kotlin

val callback = object : InputCallback {
    override fun onInputSubmitted(text: String) {
        // You will receive this callback when the user presses Enter on the keyboard.
    }

    override fun onInputTextChanged(text: String) {
        // You will receive this callback as the user is typing. The update
        // frequency is determined by the host.
    }
}

Java

InputCallback callback = new InputCallback() {
    @Override
    public void onInputSubmitted(@NonNull String text) {
        // You will receive this callback when the user presses Enter on the keyboard.
    }

    @Override
    public void onInputTextChanged(@NonNull String text) {
        // You will receive this callback as the user is typing. The update
        // frequency is determined by the host.
    }
};

InputSignInMethod Builder 需要 InputCallback

Kotlin

val passwordInput = InputSignInMethod.Builder(callback)
    .setHint("Password")
    .setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD)
    ...
    .build()

Java

InputSignInMethod passwordInput = new InputSignInMethod.Builder(callback)
    .setHint("Password")
    .setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD)
    ...
    .build();

最后,使用您的新 InputSignInMethod 创建 SignInTemplate

Kotlin

SignInTemplate.Builder(passwordInput)
    .setTitle("Sign in with username and password")
    .setInstructions("Enter your password")
    .setHeaderAction(Action.BACK)
    ...
    .build()

Java

new SignInTemplate.Builder(passwordInput)
    .setTitle("Sign in with username and password")
    .setInstructions("Enter your password")
    .setHeaderAction(Action.BACK)
    ...
    .build();

使用 AccountManager

具有身份验证的 Android Automotive OS 应用必须使用 AccountManager,原因如下:

  • 提升用户体验并简化账号管理:用户可以通过系统设置中的账号菜单轻松管理其所有账号,包括管理登录和退出。
  • “访客”体验:由于汽车是共用设备,因此 OEM 可以在车辆中启用“访客”体验,在这种体验模式下,无法添加账号。

添加文本字符串变体

不同尺寸的车载显示屏可以显示不同的文本量。利用汽车应用 API 2 及更高级别,您可以为文本字符串指定多个变体,以最佳适配屏幕。如需了解接受文本变体的位置,请查找采用 CarText 的模板和组件。

您可以使用 CarText.Builder.addVariant() 方法将文本字符串变体添加到 CarText

Kotlin

val itemTitle = CarText.Builder("This is a very long string")
    .addVariant("Shorter string")
    ...
    .build()

Java

CarText itemTitle = new CarText.Builder("This is a very long string")
    .addVariant("Shorter string")
    ...
    .build();

然后,您可以将此 CarText(举例来说)用作 GridItem 的主要文本。

Kotlin

GridItem.Builder()
    .addTitle(itemTitle)
    ...
    .build()

Java

new GridItem.Builder()
    .addTitle(itemTitle)
    ...
    build();

按照优先级从高到低的顺序(例如从最长到最短)添加字符串。主机会根据车载显示屏上的可用空间大小选择相应长度的字符串。

为各行添加内嵌式 CarIcon

您可以使用 CarIconSpan 添加内嵌文本的图标,从而提升应用的视觉吸引力。如需详细了解如何创建这些 span,请参阅有关 CarIconSpan.create 的文档。如需简要了解使用 span 设置文本样式的原理,请参阅使用 span 设置文本样式

Kotlin

  
val rating = SpannableString("Rating: 4.5 stars")
rating.setSpan(
    CarIconSpan.create(
        // Create a CarIcon with an image of four and a half stars
        CarIcon.Builder(...).build(),
        // Align the CarIcon to the baseline of the text
        CarIconSpan.ALIGN_BASELINE
    ),
    // The start index of the span (index of the character '4')
    8,
    // The end index of the span (index of the last 's' in "stars")
    16,
    Spanned.SPAN_INCLUSIVE_INCLUSIVE
)

val row = Row.Builder()
    ...
    .addText(rating)
    .build()
  
  

Java

  
SpannableString rating = new SpannableString("Rating: 4.5 stars");
rating.setSpan(
        CarIconSpan.create(
                // Create a CarIcon with an image of four and a half stars
                new CarIcon.Builder(...).build(),
                // Align the CarIcon to the baseline of the text
                CarIconSpan.ALIGN_BASELINE
        ),
        // The start index of the span (index of the character '4')
        8,
        // The end index of the span (index of the last 's' in "stars")
        16,
        Spanned.SPAN_INCLUSIVE_INCLUSIVE
);
Row row = new Row.Builder()
        ...
        .addText(rating)
        .build();
  
  

Car Hardware API

从 Car App API 级别 3 开始,汽车应用库提供可用于访问车辆属性和传感器的 API。

要求

若要将 API 与 Android Auto 搭配使用,首先要将 androidx.car.app:app-projected 的依赖项添加到 Android Auto 模块的 build.gradle 文件中。对于 Android Automotive OS,将 androidx.car.app:app-automotive 的依赖项添加到 Android Automotive OS 模块的 build.gradle 文件中。

此外,在您的 AndroidManifest.xml 文件中,您需要声明请求您要使用的汽车数据所需的相关权限。请注意,这些权限还必须由用户授予您。您可以在 Android Auto 和 Android Automotive OS 上使用相同的代码,无需创建依赖于平台的流程。但是,所需的权限有所不同。

CarInfo

下表描述了 CarInfo API 显示的属性以及使用它们所需请求的权限:

方法 属性 Android Auto 权限 Android Automotive OS 权限 从哪个 Car App API 级别开始支持
fetchModel 品牌、型号、年份 android.car.permission.CAR_INFO 3
fetchEnergyProfile EV 连接器类型、燃料类型 com.google.android.gms.permission.CAR_FUEL android.car.permission.CAR_INFO 3
fetchExteriorDimensions

此数据仅适用于搭载 API 30 或更高版本的部分 Android Automotive OS 车辆

外部尺寸 不适用 android.car.permission.CAR_INFO 7
addTollListener
removeTollListener
收费卡状态、收费卡类型 3
addEnergyLevelListener
removeEnergyLevelListener
电池电量、油量、油量较低、剩余可行驶距离 com.google.android.gms.permission.CAR_FUEL android.car.permission.CAR_ENERGY
android.car.permission.CAR_ENERGY_PORTS
android.car.permission.READ_CAR_DISPLAY_UNITS
3
addSpeedListener
removeSpeedListener
原始速度、显示速度(显示在汽车的仪表板屏幕上) com.google.android.gms.permission.CAR_SPEED android.car.permission.CAR_SPEED
android.car.permission.READ_CAR_DISPLAY_UNITS
3
addMileageListener
removeMileageListener
里程表距离 com.google.android.gms.permission.CAR_MILEAGE 在 Android Automotive OS 上,从 Play 商店安装的应用无法使用这些数据。 3

例如,若要获取剩余可行驶距离,可实例化 CarInfo 对象,然后创建并注册 OnCarDataAvailableListener

Kotlin

val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo

val listener = OnCarDataAvailableListener<EnergyLevel> { data ->
    if (data.rangeRemainingMeters.status == CarValue.STATUS_SUCCESS) {
      val rangeRemaining = data.rangeRemainingMeters.value
    } else {
      // Handle error
    }
  }

carInfo.addEnergyLevelListener(carContext.mainExecutor, listener)

// Unregister the listener when you no longer need updates
carInfo.removeEnergyLevelListener(listener)

Java

CarInfo carInfo = getCarContext().getCarService(CarHardwareManager.class).getCarInfo();

OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
  if(data.getRangeRemainingMeters().getStatus() == CarValue.STATUS_SUCCESS) {
    float rangeRemaining = data.getRangeRemainingMeters().getValue();
  } else {
    // Handle error
  }
};

carInfo.addEnergyLevelListener(getCarContext().getMainExecutor(), listener);

// Unregister the listener when you no longer need updates
carInfo.removeEnergyLevelListener(listener);

请勿假设汽车的数据始终可用。如果出现错误,请检查您请求的值的状态,从而更好地了解无法检索您请求的数据的原因。如需获取完整的 CarInfo 类定义,请参阅参考文档

CarSensors

CarSensors 类使您可以访问车辆的加速度计、陀螺仪、指南针和位置数据。这些值的可用性可能取决于 OEM。来自加速度计、陀螺仪和指南针的数据的格式与从 SensorManager API 获取的数据的格式相同。例如,若要检查车辆的朝向:

Kotlin

val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors

val listener = OnCarDataAvailableListener<Compass> { data ->
    if (data.orientations.status == CarValue.STATUS_SUCCESS) {
      val orientation = data.orientations.value
    } else {
      // Data not available, handle error
    }
  }

carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, carContext.mainExecutor, listener)

// Unregister the listener when you no longer need updates
carSensors.removeCompassListener(listener)

Java

CarSensors carSensors = getCarContext().getCarService(CarHardwareManager.class).getCarSensors();

OnCarDataAvailableListener<Compass> listener = (data) -> {
  if (data.getOrientations().getStatus() == CarValue.STATUS_SUCCESS) {
    List<Float> orientations = data.getOrientations().getValue();
  } else {
    // Data not available, handle error
  }
};

carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, getCarContext().getMainExecutor(),
    listener);

// Unregister the listener when you no longer need updates
carSensors.removeCompassListener(listener);

若要访问汽车的位置数据,您还需要声明和请求 android.permission.ACCESS_FINE_LOCATION 权限。

测试

若要在 Android Auto 上进行测试时模拟传感器数据,请参阅桌面车机指南的传感器传感器配置部分。若要在 Android Automotive OS 上进行测试时模拟传感器数据,请参阅 Android Automotive OS 模拟器指南的模拟硬件状态部分。

CarAppService、Session 和 Screen 的生命周期

SessionScreen 类实现了 LifecycleOwner 接口。当用户与应用交互时,系统将调用 SessionScreen 对象的生命周期回调,如下图所示。

CarAppService 和 Session 的生命周期

图 1. Session 生命周期。

如需了解完整详情,请参阅 Session.getLifecycle 方法的文档。

Screen 的生命周期

图 2. Screen 生命周期。

如需了解完整详情,请参阅 Screen.getLifecycle 方法的文档。

使用汽车麦克风录音

借助应用的 CarAppServiceCarAudioRecord API,您可以授予应用访问用户汽车麦克风的权限。用户需要授予您的应用访问汽车麦克风的权限。您的应用可以记录和处理用户在应用内的输入。

录音权限

在录制任何音频之前,您必须首先在 AndroidManifest.xml 中声明录音权限并请求用户授予该权限。

<manifest ...>
   ...
   <uses-permission android:name="android.permission.RECORD_AUDIO" />
   ...
</manifest>

您需要在运行时请求录音权限。如需详细了解如何在汽车应用中请求权限,请参阅请求权限部分。

录音

用户向您授予录音权限后,您就可以录制音频并处理录音。

Kotlin

val carAudioRecord = CarAudioRecord.create(carContext)
        carAudioRecord.startRecording()

        val data = ByteArray(CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE)
        while(carAudioRecord.read(data, 0, CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) >= 0) {
            // Use data array
            // Potentially call carAudioRecord.stopRecording() if your processing finds end of speech
        }
        carAudioRecord.stopRecording()
 

Java

CarAudioRecord carAudioRecord = CarAudioRecord.create(getCarContext());
        carAudioRecord.startRecording();

        byte[] data = new byte[CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE];
        while (carAudioRecord.read(data, 0, CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) >= 0) {
            // Use data array
            // Potentially call carAudioRecord.stopRecording() if your processing finds end of speech
        }
        carAudioRecord.stopRecording();
 

音频焦点

使用汽车麦克风录制音频时,首先要获取音频焦点,以确保停止所有正在播放的媒体。如果您丢失了音频焦点,请停止录音。

以下示例说明了如何获取音频焦点:

Kotlin

 
val carAudioRecord = CarAudioRecord.create(carContext)
        
        // Take audio focus so that user's media is not recorded
        val audioAttributes = AudioAttributes.Builder()
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            // Use the most appropriate usage type for your use case
            .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
            .build()
        
        val audioFocusRequest =
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
                .setAudioAttributes(audioAttributes)
                .setOnAudioFocusChangeListener { state: Int ->
                    if (state == AudioManager.AUDIOFOCUS_LOSS) {
                        // Stop recording if audio focus is lost
                        carAudioRecord.stopRecording()
                    }
                }
                .build()
        
        if (carContext.getSystemService(AudioManager::class.java)
                .requestAudioFocus(audioFocusRequest)
            != AudioManager.AUDIOFOCUS_REQUEST_GRANTED
        ) {
            // Don't record if the focus isn't granted
            return
        }
        
        carAudioRecord.startRecording()
        // Process the audio and abandon the AudioFocusRequest when done

Java

CarAudioRecord carAudioRecord = CarAudioRecord.create(getCarContext());
        // Take audio focus so that user's media is not recorded
        AudioAttributes audioAttributes =
                new AudioAttributes.Builder()
                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                        // Use the most appropriate usage type for your use case
                        .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
                        .build();

        AudioFocusRequest audioFocusRequest =
                new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
                        .setAudioAttributes(audioAttributes)
                        .setOnAudioFocusChangeListener(state -> {
                            if (state == AudioManager.AUDIOFOCUS_LOSS) {
                                // Stop recording if audio focus is lost
                                carAudioRecord.stopRecording();
                            }
                        })
                        .build();

        if (getCarContext().getSystemService(AudioManager.class).requestAudioFocus(audioFocusRequest)
                != AUDIOFOCUS_REQUEST_GRANTED) {
            // Don't record if the focus isn't granted
            return;
        }

        carAudioRecord.startRecording();
        // Process the audio and abandon the AudioFocusRequest when done
 

测试库

Android for Cars 测试库提供了一些辅助类,可用于在测试环境中验证应用的行为。例如,借助 SessionController,您可以模拟与主机的连接,验证是否创建并返回正确的 ScreenTemplate

请参阅示例,查看使用示例。

报告 Android for Cars 应用库问题

如果您发现该库存在问题,请使用 Google 问题跟踪器报告该问题。请务必在问题模板中填写所有必填信息。

创建新问题

在提交新问题之前,请先查看该问题是否已在库的版本说明中列出或在问题列表中报告。您可以在跟踪器中点击问题的星标来对问题进行订阅和投票。如需了解详情,请参阅订阅问题