AAOS 中的系统特权应用可以使用多显示器通信 API 与在车内不同乘员区域中运行的同一应用(相同的软件包名称)进行通信。本页面介绍了如何集成该 API。要了解详情,您还可以参阅 CarOccupantZoneManager.OccupantZoneInfo。
乘员区域
乘员区域的概念将用户映射到一组显示屏。每个乘员区域都有一个类型为 DISPLAY_TYPE_MAIN 的显示屏。乘员区域也可能具有其他显示屏,例如仪表盘显示屏。每个乘员区域都分配有一个 Android 用户。每个用户都有自己的帐号和应用。
硬件配置
Comms API 仅支持单个 SoC。在单 SoC 模型中,所有乘员区域和用户都在同一 SoC 上运行。Comms API 由三个组件组成:
电源管理 API 允许客户端管理乘员区域中显示屏的电源。
Discovery API 允许客户端监控车内其他乘员区域的状态,以及监控这些乘员区域中的对等客户端。在使用 Connection API 之前,请先使用 Discovery API。
Connection API 允许客户端连接到另一个乘员区域中的对等客户端,并将有效负载发送到对等客户端。
连接需要 Discovery API 和 Connection API。电源管理 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>
获取 Car 管理器
要使用该 API,客户端应用必须注册 CarServiceLifecycleListener
以获取关联的 Car 管理器。
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
,则发送方可以显示 Toast 消息或对话框,以提示用户解锁接收器乘员区域的屏幕。
当发送方不再需要发现接收器时(例如,当它找到所有接收器并建立连接或变为不活跃状态时),它可以停止发现。
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);
}
}
发送方可以在 Payload
中放置 Binder
对象或字节数组。如果发送方需要发送其他数据类型,则必须将数据序列化为字节数组,使用该字节数组构造 Payload
对象,然后发送 Payload
。然后,接收器客户端从收到的 Payload
中获取字节数组,并将字节数组反序列化为预期的数据对象。例如,如果发送方想要向 ID 为 FragmentB
的接收器端点发送字符串 hello
,则可以使用 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
在客户端应用中是唯一的。receiverEndpointId
将由 AbstractReceiverService
用于决定要将 Payload 分派到哪个或哪些接收器端点。例如:
- 发送方在
Payload
中指定receiver_endpoint_id:FragmentB
。当接收到Payload
时,接收器中的AbstractReceiverService
会调用forwardPayload("FragmentB", payload)
以将 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
。当汽车服务重启后,这两个管理器会设置为非空值。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 发送 Payload,但反之则不行
连接是单向的(按设计)。要建立双向连接,client1
和 client2
都必须互相请求连接,然后获得批准。