多显示器通信 API

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_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,则发送方可以显示 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 流。

Send the 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 在客户端应用中是唯一的。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. 说明了连接流程。

Connection flow

图 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 为空值

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

  1. 汽车服务崩溃。如前所述,当汽车服务崩溃时,这两个管理器会特意重置为空值。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 发送 Payload,但反之则不行

连接是单向的(按设计)。要建立双向连接,client1client2 都必须互相请求连接,然后获得批准。