使用 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

安装 Car App 库

有关如何将库添加到您的应用的说明,请参阅 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>元素指定的那些值。

设置自定义主题

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

Car App API 级别

Car App 库定义了自己的 API 级别,以便您可以知道车辆上的模板主机支持哪些库功能。要检索主机支持的最高 Car App API 级别,请使用getCarAppApiLevel()方法。

在您的AndroidManifest.xml文件中声明您的应用支持的最低 Car App API 级别

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

有关如何维护向后兼容性和声明使用功能所需的最低 API 级别,请参阅RequiresCarApi注释的文档。有关使用 Car App 库的某个功能所需的 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 会使用第一个矩形进行调用。

支持深色主题

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

要决定是否绘制深色地图,您可以使用 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() 方法。

使用 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 扩展通知实例,该 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 并包含数据 URI 的 Intent 时,将调用该接收器来处理 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();

按从最优到最不优的顺序添加字符串——例如,从最长到最短。主机根据汽车屏幕上的可用空间选择合适的长度字符串。

为行添加内联 CarIcons

您可以使用 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 App API 级别 3 开始,Car App 库提供了一些 API,您可以使用它们来访问车辆属性和传感器。

要求

要将这些 API 与 Android Auto 一起使用,请先在 Android Auto 模块的 build.gradle 文件中添加对 androidx.car.app:app-projected 的依赖项。对于 Android Automotive OS,请在 Android Automotive OS 模块的 build.gradle 文件中添加对 androidx.car.app:app-automotive 的依赖项。

此外,在您的 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 方法的文档。

屏幕的生命周期

图 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 App 库问题

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

创建新问题

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