AAOS 中的系统特权应用可以使用 Multi-Display Communications API,以便与在汽车中另一个乘员区内运行的同一应用(软件包名称相同)进行通信。本页介绍了如何集成该 API。如需了解详情,您还可以参阅 CarOccupantZoneManager.OccupantZoneInfo。
乘员区
乘员区的概念是将一个用户映射到一组显示屏。每个乘员区都有一个类型为 DISPLAY_TYPE_MAIN 的显示屏。乘员区可能还有其他显示屏,例如仪表板显示屏。每个乘员区都分配了一个 Android 用户。每个用户都有自己的账号和应用。
硬件配置
Comms API 仅支持单个 SoC。在单个 SoC 模型中,所有乘员区和用户都在同一 SoC 上运行。Comms API 由三个组件组成:
Power management API 让客户端可以管理乘员区显示屏的电源。
Discovery API 让客户端可以监控车内其他乘员区的状态,以及监控这些乘员区中的对等客户端。请先使用 Discovery API,然后再使用 Connection API。
Connection API 让客户端可以连接到另一个乘员区中的对等客户端,并向对等客户端发送载荷。
若要连接,需使用 Discovery API 和 Connection API。Power Management API 是可选的。
Comms API 不支持不同应用之间的通信。它仅适用于具有相同软件包名称的应用之间的通信,仅用于不同可见用户之间的通信。
集成指南
实现 AbstractReceiverService
如需接收 Payload,接收方应用必须实现 AbstractReceiverService 中定义的抽象方法。例如:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
当发送方客户端请求与此接收方客户端建立连接时,系统会调用 onConnectionInitiated()。如果需要用户确认才能建立连接,MyReceiverService 可以替换此方法以启动权限 activity,并根据结果调用 acceptConnection() 或 rejectConnection()。否则,MyReceiverService 只需调用 acceptConnection()。`
当 MyReceiverService 收到来自发送方客户端的 Payload 时,系统会调用 onPayloadReceived()。MyReceiverService 可以替换此方法,以便:
- 将
Payload转发到相应的接收方端点(如果有)。如需获取已注册的接收方端点,请调用getAllReceiverEndpoints()。如需将Payload转发到给定的接收方端点,请调用forwardPayload()
或,
- 缓存
Payload,并在预期的接收方端点注册后发送荷载,为此MyReceiverService会通过onReceiverRegistered()收到通知
声明 AbstractReceiverService
接收方应用必须在其清单文件中声明已实现的 AbstractReceiverService,为此服务添加一个包含 android.car.intent.action.RECEIVER_SERVICE 操作的 intent 过滤器,并要求具备 android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE 权限:
<service android:name=".MyReceiverService"
android:permission="android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.car.intent.action.RECEIVER_SERVICE" />
</intent-filter>
</service>
android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE 权限可确保仅该框架可以绑定到此服务。如果此服务不需要该权限,另一应用或许能够绑定到此服务并直接向其发送 Payload。
声明权限
客户端应用必须在其清单文件中声明权限。
<!-- This permission is needed for connection API -->
<uses-permission android:name="android.car.permission.MANAGE_OCCUPANT_CONNECTION"/>
<!-- This permission is needed for discovery API -->
<uses-permission android:name="android.car.permission.MANAGE_REMOTE_DEVICE"/>
<!-- This permission is needed if the client app calls CarRemoteDeviceManager#setOccupantZonePower() -->
<uses-permission android:name="android.car.permission.CAR_POWER"/>
上述三项权限都是特许权限,必须通过许可名单文件预先授予。例如,以下是 MultiDisplayTest 应用的许可名单文件:
// packages/services/Car/data/etc/com.google.android.car.multidisplaytest.xml
<permissions>
<privapp-permissions package="com.google.android.car.multidisplaytest">
… …
<permission name="android.car.permission.MANAGE_OCCUPANT_CONNECTION"/>
<permission name="android.car.permission.MANAGE_REMOTE_DEVICE"/>
<permission name="android.car.permission.CAR_POWER"/>
</privapp-permissions>
</permissions>
获取汽车管理器
如需使用该 API,客户端应用必须注册 CarServiceLifecycleListener 以获取关联的汽车管理器:
private CarRemoteDeviceManager mRemoteDeviceManager;
private CarOccupantConnectionManager mOccupantConnectionManager;
private final Car.CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
if (!ready) {
Log.w(TAG, "Car service crashed");
mRemoteDeviceManager = null;
mOccupantConnectionManager = null;
return;
}
mRemoteDeviceManager = car.getCarManager(CarRemoteDeviceManager.class);
mOccupantConnectionManager = car.getCarManager(CarOccupantConnectionManager.class);
};
Car.createCar(getContext(), /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
mCarServiceLifecycleListener);
(发送方)发现
在连接到接收方客户端之前,发送方客户端应通过注册 CarRemoteDeviceManager.StateCallback 发现接收方客户端:
// The maps are accessed by the main thread only, so there is no multi-thread issue.
private final ArrayMap<OccupantZoneInfo, Integer> mOccupantZoneStateMap = new ArrayMap<>();
private final ArrayMap<OccupantZoneInfo, Integer> mAppStateMap = new ArrayMap<>();
private final StateCallback mStateCallback = new StateCallback() {
@Override
public void onOccupantZoneStateChanged(
@androidx.annotation.NonNull OccupantZoneInfo occupantZone,
int occupantZoneStates) {
mOccupantZoneStateMap.put(occupantZone, occupantZoneStates);
}
@Override
public void onAppStateChanged(
@androidx.annotation.NonNull OccupantZoneInfo occupantZone,
int appStates) {
mAppStateMap.put(occupantZone, appStates);
}
};
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.registerStateCallback(getActivity().getMainExecutor(),
mStateCallback);
}
在请求连接到接收方之前,发送方应确保接收方乘员区和接收方应用的所有标志均已设置。否则,可能会出现错误。例如:
private boolean canRequestConnectionToReceiver(OccupantZoneInfo receiverZone) {
Integer zoneState = mOccupantZoneStateMap.get(receiverZone);
if ((zoneState == null) || (zoneState.intValue() & (FLAG_OCCUPANT_ZONE_POWER_ON
// FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED is not implemented yet. Right now
// just ignore this flag.
// | FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
| FLAG_OCCUPANT_ZONE_CONNECTION_READY)) == 0) {
return false;
}
Integer appState = mAppStateMap.get(receiverZone);
if ((appState == null) ||
(appState.intValue() & (FLAG_CLIENT_INSTALLED
| FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE
| FLAG_CLIENT_RUNNING | FLAG_CLIENT_IN_FOREGROUND)) == 0) {
return false;
}
return true;
}
我们建议,发送方仅在接收方的所有标志均已设置的情况下请求连接到接收方。不过,也有一些例外情况:
FLAG_OCCUPANT_ZONE_CONNECTION_READY和FLAG_CLIENT_INSTALLED是建立连接所需满足的最低要求。如果接收方应用需要显示界面以征得用户批准进行连接,则
FLAG_OCCUPANT_ZONE_POWER_ON和FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED成为额外的要求。为提供更好的用户体验,我们还建议您使用FLAG_CLIENT_RUNNING和FLAG_CLIENT_IN_FOREGROUND,否则用户可能会感到意外。目前 (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED尚未实现。客户端应用可以忽略它。目前(Android 15),Comms API 仅支持在同一个 Android 实例上的多个用户,以便对等应用可以具有相同的长版本代码 (
FLAG_CLIENT_SAME_LONG_VERSION) 和签名 (FLAG_CLIENT_SAME_SIGNATURE)。因此,应用无需验证这两个值是否一致。
为了提供更好的用户体验,如果未设置标志,发送方客户端可以显示界面。例如,如果未设置 FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED,发送方可以显示一个消息框或对话框,提示用户解锁接收方乘员区的屏幕。
当发送方不再需要发现接收方时(例如当它找到所有接收方并建立连接或进入不活跃状态时),可以停止发现。
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
停止发现后,现有连接不受影响。发送方可以继续向已连接的接收方发送 Payload。
(发送方)请求连接
当接收方的所有标志都已设置时,发送方可以请求连接到接收方:
private final ConnectionRequestCallback mRequestCallback = new ConnectionRequestCallback() {
@Override
public void onConnected(OccupantZoneInfo receiverZone) {
}
@Override
public void onFailed(OccupantZoneInfo receiverZone, int connectionError) {
}
@Override
public void onDisconnected(OccupantZoneInfo receiverZone) {
}
};
if (mOccupantConnectionManager != null && canRequestConnectionToReceiver(receiverZone)) {
mOccupantConnectionManager.requestConnection(receiverZone,
getActivity().getMainExecutor(), mRequestCallback);
}
(接收方服务)接受连接
发送方请求连接到接收方后,接收方应用中的 AbstractReceiverService 将由汽车服务绑定,AbstractReceiverService.onConnectionInitiated() 将被调用。如(发送方)请求连接中所述,onConnectionInitiated() 是一种抽象方法,必须由客户端应用实现。
当接收方接受连接请求后,系统会调用发送方的 ConnectionRequestCallback.onConnected(),然后建立连接。
(发送方)发送载荷
建立连接后,发送方可以向接收方发送 Payload:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
发送方可以将 Binder 对象或字节数组放入 Payload 中。如果发送方需要发送其他类型的数据,则必须将数据序列化为字节数组,使用字节数组构建 Payload 对象,然后发送 Payload。然后,接收方客户端从收到的 Payload 中获取字节数组,并将该字节数组反序列化为预期的数据对象。例如,如果发送方希望将字符串 hello 发送到 ID 为 FragmentB 的接收方端点,则可以使用 Proto Buffers 定义一个数据类型,如下所示:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
图 1 展示了 Payload 流程:
(接收方服务)接收并分派载荷
接收方应用收到 Payload 后,系统会调用其 AbstractReceiverService.onPayloadReceived()。如发送载荷中所述,onPayloadReceived() 是一种抽象方法,必须由客户端应用实现。在此方法中,客户端可以将 Payload 转发到相应的接收方端点,或将 Payload 缓存起来,然后在预期的接收方端点注册后发送它。
(接收方端点)注册和取消注册
接收方应用应调用 registerReceiver() 来注册接收方端点。一个典型的用例是,fragment 需要接收 Payload,因此它会注册接收方端点:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
接收方客户端中的 AbstractReceiverService 将 Payload 分派到接收方端点后,系统会调用关联的 PayloadCallback。
客户端应用可以注册多个接收方端点,只要它们的 receiverEndpointId 在客户端应用中是唯一的。AbstractReceiverService 将使用 receiverEndpointId 来决定将载荷分派到哪个接收方端点。例如:
- 发送方在
Payload中指定receiver_endpoint_id:FragmentB。接收Payload后,接收方中的AbstractReceiverService会调用forwardPayload("FragmentB", payload)以将载荷分派给FragmentB - 发送方在
Payload中指定data_type:VOLUME_CONTROL。接收Payload后,接收方中的AbstractReceiverService知道此类Payload应分派给FragmentB,因此会调用forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(发送方)终止连接
当发送方不再需要向接收方发送 Payload 时(例如它进入不活跃状态),应终止连接。
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
断开连接后,发送方将无法再向接收方发送 Payload。
连接流程
连接流程如图 2 所示。
问题排查
检查日志
如需检查相应日志,请执行以下操作:
运行以下命令以进行日志记录:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"如需转储
CarRemoteDeviceService和CarOccupantConnectionService的内部状态,请执行以下操作:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
CarRemoteDeviceManager 和 CarOccupantConnectionManager 变为 null
请检查以下可能的根本原因:
汽车服务崩溃。如前所述,当汽车服务发生崩溃时,这两个管理器会被故意重置为
null。汽车服务重启后,这两个管理器会设为非 null 值。CarRemoteDeviceService或CarOccupantConnectionService未启用。如需确定其中一个或另一个是否已启用,请运行:adb shell dumpsys car_service --services CarFeatureController查找
mDefaultEnabledFeaturesFromConfig,其中应包含car_remote_device_service和car_occupant_connection_service。例如:mDefaultEnabledFeaturesFromConfig:[car_evs_service, car_navigation_service, car_occupant_connection_service, car_remote_device_service, car_telemetry_service, cluster_home_service, com.android.car.user.CarUserNoticeService, diagnostic, storage_monitoring, vehicle_map_service]默认情况下,这两项服务处于停用状态。如果设备支持多屏幕,您必须叠加此配置文件。您可以在配置文件中启用这两项服务:
// packages/services/Car/service/res/values/config.xml <string-array translatable="false" name="config_allowed_optional_car_features"> <item>car_occupant_connection_service</item> <item>car_remote_device_service</item> … … </string-array>
调用 API 时出现异常
如果客户端应用未按预期使用该 API,则可能会发生异常。在这种情况下,客户端应用可以检查异常中的消息和崩溃堆栈,以便解决问题。滥用 API 的示例包括:
registerStateCallback():此客户端已注册StateCallback。unregisterStateCallback():此CarRemoteDeviceManager实例未注册任何StateCallback。registerReceiver()receiverEndpointId已经注册了。unregisterReceiver():receiverEndpointId未注册。requestConnection():已存在待处理或已建立的连接。cancelConnection():没有待处理的连接可取消。sendPayload():未建立连接。disconnect():未建立连接。
Client1 可以向 client2 发送载荷,但不能反向发送
连接是单向的,这是设计上的有意为之。如需建立双向连接,client1 和 client2 都必须相互请求连接,然后获得批准。