使用 Android for Cars 应用库

Android for Cars 应用库 允许您将导航、兴趣点 (POI) 和物联网 (IOT) 应用带到汽车中。它通过提供一套旨在满足驾驶员分心标准的模板并处理各种汽车屏幕因素和输入方式等细节来实现此目的。

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

开始之前

  1. 查看涵盖汽车应用库的 驾驶设计 页面
  2. 查看以下部分中的 关键术语和概念
  3. 熟悉 Android Auto 系统 UIAndroid 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 实现连接到您的应用。您在清单中声明此服务,以便主机发现并连接到您的应用。

您还需要在应用的意图过滤器中的<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(如上一节所述)时,通过在意图过滤器中添加以下一个或多个类别值来声明应用的类别

  • androidx.car.app.category.NAVIGATION:提供转向导航方向的应用。有关此类别的更多文档,请查看为汽车构建导航应用
  • androidx.car.app.category.POI:提供与查找兴趣点(例如停车位、充电站和加油站)相关的功能的应用。有关此类别的更多文档,请查看为汽车构建兴趣点应用
  • androidx.car.app.category.IOT:允许用户在车内对连接的设备执行相关操作的应用。有关此类别的更多文档,请查看为汽车构建物联网应用

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

指定应用名称和图标

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

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

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

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

设置自定义主题

要为汽车应用设置自定义主题,请在清单文件中添加<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 实例。

以下代码片段显示了如何声明一个使用PaneTemplate 模板显示简单的“Hello world!”字符串的Screen

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 还提供其他功能,例如允许您使用汽车屏幕的配置加载可绘制资源,使用意图在汽车中启动应用,以及指示您的应用是否应以深色主题显示其地图。

实现屏幕导航

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

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 来绘制地图

模板 模板权限 类别指南
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 导航

声明界面权限

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

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

访问界面

要访问主机提供的Surface,您必须实现一个SurfaceCallback并将其提供给AppManager汽车服务。当前的Surface将通过onSurfaceAvailable()onSurfaceDestroyed()回调的SurfaceContainer参数传递给您的SurfaceCallback

Kotlin

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

Java

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

了解界面的可见区域

主机可以在地图顶部为模板绘制用户界面元素。主机通过调用SurfaceCallback.onVisibleAreaChanged方法来传达保证无遮挡且对用户完全可见的界面区域。此外,为了最大程度地减少更改次数,主机将使用最小矩形调用SurfaceCallback.onStableAreaChanged方法,该矩形始终基于当前模板可见。

例如,当导航应用使用带有顶部操作栏NavigationTemplate时,当用户一段时间内未与屏幕交互时,操作栏可以隐藏自身以腾出更多空间用于地图。在这种情况下,会向onStableAreaChangedonVisibleAreaChanged回调相同的矩形。当操作栏隐藏时,只有onVisibleAreaChanged会使用更大的区域被调用。如果用户与屏幕交互,则再次仅调用onVisibleAreaChanged,并使用第一个矩形。

支持深色主题

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

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

允许用户与您的地图交互

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

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

实现交互性回调

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

交互 SurfaceCallback方法 自 Car 应用 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意图的格式)的更多详细信息,请参阅使用意图启动汽车应用部分。

某些操作(例如需要指示用户在他们的移动设备上继续交互的操作)仅在汽车停放时才允许。您可以使用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 设计指南,了解有关通知的内容。

显示吐司

您的应用可以使用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 启动您自己的应用。

以下示例演示了如何创建一个带有操作的通知,该操作使用显示停车预订详细信息的屏幕打开您的应用。您使用包含 PendingIntent 的内容 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,当用户在通知界面中选择操作并调用 CarContext.startCarApp 且 Intent 包含数据 URI 时,将调用该接收器来处理 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 等模板(使用 Car 应用 API 级别 2 及更高版本)来处理在汽车的车载主机上登录您的应用。

要创建 SignInTemplate,请定义一个 SignInMethod。Car 应用库当前支持以下登录方法

例如,要实现一个收集用户密码的模板,首先创建一个 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 可以启用车辆中的访客体验,在该体验中,无法添加帐户。

添加文本字符串变体

不同的汽车屏幕尺寸可能显示不同数量的文本。使用 Car 应用 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 将图标与文本内联添加,以丰富应用的视觉吸引力。有关创建这些跨度的更多信息,请参阅 CarIconSpan.create 的文档。有关使用跨度进行文本样式设置的工作原理概述,请参阅 使用跨度进行精彩的文本样式设置

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

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

要求

要在 Android Auto 中使用这些 API,请首先将对 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 应用 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 车辆上可用

外部尺寸 N/A 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 测试库 提供了辅助类,您可以使用这些类在测试环境中验证应用的行为。例如,SessionController 允许您模拟与主机的连接,并验证是否创建并返回了正确的 ScreenTemplate

有关用法示例,请参阅 示例

报告面向汽车的 Android 应用库问题

如果您发现库存在问题,请使用 Google Issue Tracker 报告。请务必在问题模板中填写所有必需的信息。

创建新问题

在提交新问题之前,请检查库的发行说明或问题列表中是否已列出该问题。您可以通过点击跟踪器中问题旁边的星号来订阅和投票。有关更多信息,请参阅 订阅问题