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()
。`
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
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
中获取字节数组,并将该字节数组反序列化为预期的数据对象。例如,如果发送方希望向 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
在客户端应用中是唯一的。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
都必须相互请求连接,然后获得批准。