使用 Android for Cars 应用库

Android for Cars 应用库让您可以将您的导航兴趣点 (POI)物联网 (IOT)天气应用带到汽车中。它通过提供一套旨在满足驾驶员注意力分散标准的模板,并处理各种汽车屏幕因素和输入模式等细节来实现这一点。

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

开始之前

  1. 查看涵盖车载应用库的驾驶设计页面
  2. 查看以下部分中的关键术语和概念
  3. 熟悉Android Auto 系统界面Android Automotive OS 设计
  4. 查看版本说明
  5. 查看示例

关键术语和概念

模型和模板
用户界面由模型对象图表示,这些对象可以根据其所属的模板以不同方式进行排列。模板是那些可以作为图中根节点的一部分模型。模型包含以文本和图像形式显示给用户的信息,以及用于配置此类信息视觉外观方面(例如,文本颜色或图像大小)的属性。宿主会将模型转换为旨在满足驾驶员注意力分散标准的视图,并处理各种汽车屏幕因素和输入模式等细节。
宿主
宿主是实现库 API 提供功能的后端组件,以便您的应用可以在汽车中运行。宿主的职责包括发现您的应用和管理其生命周期,将您的模型转换为视图,以及将用户交互通知您的应用。在移动设备上,此宿主由 Android Auto 实现。在 Android Automotive OS 上,此宿主作为系统应用安装。
模板限制
不同的模板对其模型内容强制执行限制。例如,列表模板对可以呈现给用户的项目数量有限制。模板在连接以形成任务流的方式上也有限制。例如,应用最多只能将五个模板推送到屏幕堆栈。有关详细信息,请参阅模板限制
屏幕
Screen 是库提供的一个类,应用实现此来管理呈现给用户的用户界面。Screen 具有生命周期,并提供了应用在屏幕可见时发送要显示的模板的机制。Screen 实例也可以推送到和弹出到Screen 堆栈,这确保它们遵守模板流限制
CarAppService
CarAppService 是一个抽象的Service 类,您的应用必须实现并导出它才能被宿主发现和管理。您的应用的CarAppService 负责使用createHostValidator 验证宿主连接是否可信,然后使用onCreateSession 为每个连接提供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 过滤器中添加以下一个或多个类别值来声明您的应用类别

有关每个类别的详细说明以及应用所属类别的标准,请参阅车载 Android 应用质量

指定应用名称和图标

您需要指定宿主可用于在系统界面中表示您的应用的应用名称和图标。

您可以使用labelicon 属性指定用于表示您的应用的名称和图标,这些属性属于您的CarAppService

...
<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 方法的类来创建应用显示的屏幕,该方法返回表示要在车载屏幕中显示的 UI 状态的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 类是您的SessionScreen 实例可访问的ContextWrapper 子类。它提供了对车载服务的访问权限,例如用于管理屏幕堆栈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 对象是一个标准Action,它会自动调用ScreenManager.pop。此行为可以通过使用CarContext 中可用的OnBackPressedDispatcher 实例来覆盖。

为了帮助确保应用在驾驶时安全使用,屏幕堆栈的最大深度为五个屏幕。有关详细信息,请参阅模板限制部分。

刷新模板内容

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

刷新Screen 时,了解模板中可以更新的特定内容很重要,这样宿主就不会将新模板计入模板配额。有关详细信息,请参阅模板限制部分。

我们建议您构建屏幕,使Screen 与其通过onGetTemplate 实现返回的模板类型之间存在一对一映射。

绘制地图

使用以下模板的导航、兴趣点 (POI) 和天气应用可以通过访问Surface 绘制地图。

要使用以下模板,您的应用必须在其AndroidManifest.xml 文件中声明一个<uses-permission> 元素中的相应权限之一。

模板 模板权限 类别指导
NavigationTemplate androidx.car.app.NAVIGATION_TEMPLATES 导航
MapWithContentTemplate androidx.car.app.NAVIGATION_TEMPLATES
androidx.car.app.MAP_TEMPLATES
导航POI天气
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 的调用。

在仪表盘显示屏上绘制地图

除了在主显示屏上绘制地图外,导航应用还可以支持在方向盘后面的仪表盘显示屏上绘制地图。有关其他指导,请参阅绘制到仪表盘显示屏

允许用户与您的地图互动

使用以下模板时,您可以添加对用户与您绘制的地图进行交互的支持,例如让他们通过缩放和平移查看地图的不同部分。

模板 自车载应用 API 级别以来支持的交互
NavigationTemplate 2
PlaceListNavigationTemplate (已弃用) 4
RoutePreviewNavigationTemplate (已弃用) 4
MapTemplate (已弃用) 5(模板引入)
MapWithContentTemplate 7(模板引入)

实现交互性回调

SurfaceCallback 接口有几个回调方法,您可以实现这些方法以向使用上一节中的模板构建的地图添加交互性

交互 SurfaceCallback 方法 自车载应用 API 级别以来支持
点击 onClick 5
双指缩放 onScale 2
单点触摸拖动 onScroll 2
单点触摸甩动 onFling 2
双击 onScale(缩放因子由模板宿主确定) 2
平移模式下的旋转轻推 onScroll(距离因子由模板宿主确定) 2

添加地图操作条

这些模板可以有一个地图操作条,用于地图相关操作,例如放大和缩小、居中、显示指南针以及您选择显示的其他操作。地图操作条最多可以有四个仅包含图标的按钮,这些按钮可以刷新而不会影响任务深度。它在空闲状态下隐藏,在活动状态下重新出现。

要接收地图交互性回调,您必须在地图操作条中添加一个Action.PAN 按钮。当用户按下平移按钮时,宿主进入平移模式,如以下部分所述。

如果您的应用在地图操作条中省略了Action.PAN 按钮,它将不会从SurfaceCallback 方法接收用户输入,并且宿主会退出任何先前激活的平移模式。

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

了解平移模式

在平移模式下,模板宿主会将来自非触摸输入设备(例如旋转控制器和触摸板)的用户输入转换为相应的SurfaceCallback 方法。通过NavigationTemplate.Builder 中的setPanModeListener 方法响应用户进入或退出平移模式的操作。当用户处于平移模式时,宿主可以隐藏模板中的其他 UI 组件。

与用户互动

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

处理用户输入

您的应用可以通过将适当的侦听器传递给支持它们的模型来响应用户输入。以下代码片段展示了如何创建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 文档中所述。

如果使用true 值调用NotificationCompat.Builder.setOnlyAlertOnce,则高优先级通知只会作为 HUN 显示一次。

有关如何设计车载应用通知的更多信息,请参阅 Google 驾驶设计指南中关于通知的部分。

显示 Toast 消息

您的应用可以使用CarToast 显示 Toast 消息,如以下代码片段所示

Kotlin

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

Java

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

请求权限

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

使用CarContext.requestPermissions() 而不是标准 Android API 的好处是,您无需启动自己的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 启动您自己的应用。

以下示例展示了如何创建带有操作的通知,该操作会打开您的应用并显示停车预订详细信息屏幕。您使用包含PendingIntent 的内容 intent 扩展通知实例,该 intent 包装了一个明确的 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()));
        }
    }
}

有关如何处理车载应用通知的更多信息,请参阅显示通知部分。

模板限制

宿主限制了给定任务可以显示的模板数量,最多为五个,其中最后一个模板必须是以下类型之一

请注意,此限制适用于模板的数量,而不适用于堆栈中Screen 实例的数量。例如,如果应用在屏幕 A 中发送了两个模板,然后推送屏幕 B,则现在可以再发送三个模板。或者,如果每个屏幕都配置为发送单个模板,则应用可以将五个屏幕实例推送到ScreenManager 堆栈。

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

模板刷新

某些内容更新不计入模板限制。通常,如果应用推送的新模板与上一个模板类型相同且包含相同的主要内容,则新模板不计入配额。例如,更新ListTemplate 中行的切换状态不计入配额。请参阅各个模板的文档,以了解哪些类型的内容更新可以被视为刷新。

回退操作

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

例如,如果应用在屏幕 A 中发送了两个模板,然后推送屏幕 B 并再发送两个模板,则应用还剩下一个配额。如果应用随后回退到屏幕 A,宿主会将配额重置为三个,因为应用回退了两个模板。

请注意,回退到屏幕时,应用必须发送与该屏幕上次发送的模板类型相同的模板。发送任何其他模板类型都会导致错误。但是,只要在回退操作期间类型保持不变,应用就可以自由修改模板内容而不影响配额。

重置操作

某些模板具有表示任务结束的特殊语义。例如,NavigationTemplate 是一个视图,预计会停留在屏幕上并使用新的逐向指令刷新以供用户使用。当它达到这些模板之一时,宿主会重置模板配额,将该模板视为新任务的第一步。这允许应用开始新任务。请参阅各个模板的文档,以了解哪些模板会在宿主上触发重置。

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

有关如何在车载屏幕中显示应用通知的更多详细信息,请参阅显示通知部分。有关如何从通知操作启动应用的更多信息,请参阅使用 intent 启动车载应用部分。

连接 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();
}

约束 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();
  
  

车载硬件 API

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

要求

要在 Android Auto 上使用这些 API,首先要在 Android Auto 模块的build.gradle 文件中添加对androidx.car.app:app-projected 的依赖。对于 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 权限 自车载应用 API 级别以来支持
fetchModel 品牌、型号、年份 android.car.permission.CAR_INFO 3
fetchEnergyProfile 电动汽车连接器类型、燃料类型 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

警告:Mileage 类的getOdometerMeters 方法命名不准确,返回的是公里而不是米。

里程表距离 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 方法的文档。

屏幕的生命周期

图 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 问题跟踪器报告。请务必填写问题模板中请求的所有信息。

创建新问题

在提交新问题之前,请检查它是否已列在库的版本说明中或已在问题列表中报告。您可以通过点击跟踪器中问题的星标来订阅和投票。有关详细信息,请参阅订阅问题