Multi-Display Communications API

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 whenMyReceiverServicehas received aPayloadfrom 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_READYFLAG_CLIENT_INSTALLED 是建立连接所需满足的最低要求。

  • 如果接收方应用需要显示界面以征得用户批准进行连接,则 FLAG_OCCUPANT_ZONE_POWER_ONFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED 成为额外的要求。为了提供更好的用户体验,我们还建议您使用 FLAG_CLIENT_RUNNINGFLAG_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 流程:

发送载荷

图 1. 发送载荷。

(接收方服务)接收并分派载荷

接收方应用收到 Payload 后,系统会调用其 AbstractReceiverService.onPayloadReceived()。如发送载荷中所述,onPayloadReceived() 是一种抽象方法,必须由客户端应用实现。在此方法中,客户端可以将 Payload 转发到相应的接收方端点,或将 Payload 缓存起来,然后在预期的接收方端点注册后发送它。

(接收方端点)注册和取消注册

接收方应用应调用 registerReceiver() 来注册接收方端点。一个典型的用例是,fragment 需要接收 Payload,因此它会注册接收方端点:

private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
    …
};

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.registerReceiver("FragmentB",
                getActivity().getMainExecutor(), mPayloadCallback);
}

接收方客户端中的 AbstractReceiverServicePayload 分派到接收方端点后,系统会调用关联的 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 所示。

连接流程

图 2. 连接流程。

问题排查

检查日志

如需检查相应日志,请执行以下操作:

  1. 运行以下命令以进行日志记录:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
    
  2. 如需转储 CarRemoteDeviceServiceCarOccupantConnectionService 的内部状态,请执行以下操作:

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
    

CarRemoteDeviceManager 和 CarOccupantConnectionManager 变为 null

请查看以下可能的根本原因:

  1. 汽车服务崩溃。如前所述,当汽车服务发生崩溃时,这两个管理器会被故意重置为 null。汽车服务重启后,这两个管理器会设为非 null 值。

  2. 未启用 CarRemoteDeviceServiceCarOccupantConnectionService。如需确定其中一个或另一个是否已启用,请运行:

    adb shell dumpsys car_service --services CarFeatureController
    
    • 查找 mDefaultEnabledFeaturesFromConfig,其中应包含 car_remote_device_servicecar_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 发送载荷,但不能反向发送

连接是单向的,这是设计使然。如需建立双向连接,client1client2 都必须相互请求连接,然后获得批准。