API многоэкранной связи

API связи с несколькими дисплеями может использоваться системным привилегированным приложением в AAOS для связи с тем же приложением (то же имя пакета), работающим в другой зоне пассажира в автомобиле. На этой странице описывается, как интегрировать API. Чтобы узнать больше, вы также можете просмотреть CarOccupantZoneManager.OccupantZoneInfo .

Зона присутствия

Концепция зоны присутствия отображает пользователя на набор дисплеев. Каждая зона присутствия имеет дисплей типа DISPLAY_TYPE_MAIN . Зона агента также может иметь дополнительные дисплеи, например, кластерный дисплей. Каждой зоне обитателя назначается пользователь Android. У каждого пользователя есть свои учетные записи и приложения.

Конфигурация оборудования

Comms API поддерживает только один SoC. В модели с единой SoC все зоны присутствия и пользователи работают на одной SoC. Comms API состоит из трех компонентов:

  • API управления питанием позволяет клиенту управлять питанием дисплеев в зонах присутствия.

  • API обнаружения позволяет клиенту отслеживать состояние других зон пассажиров в автомобиле, а также отслеживать одноранговых клиентов в этих зонах пассажиров. Используйте API обнаружения перед использованием API подключения.

  • API подключения позволяет клиенту подключаться к своему одноранговому клиенту в другой зоне и отправлять полезную нагрузку одноранговому клиенту.

Для подключения необходимы API обнаружения и 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 может переопределить этот метод, чтобы запустить действие разрешения, и вызвать acceptConnection() или rejectConnection() на основе результата. В противном случае MyReceiverService может просто вызвать acceptConnection() .`

onPayloadReceived() is invoked when MyReceiverService has received a полезную нагрузку from the sender client. MyReceiverService` может переопределить этот метод, чтобы:

  • Переслать Payload соответствующим конечным точкам получателя, если таковые имеются. Чтобы получить зарегистрированные конечные точки получателя, вызовите getAllReceiverEndpoints() . Чтобы переслать Payload в заданную конечную точку получателя, вызовите forwardPayload()

ИЛИ,

  • Кэшируйте Payload и отправляйте его, когда будет зарегистрирована ожидаемая конечная точка получателя, о чем MyReceiverService уведомляется через onReceiverRegistered()

Объявить AbstractReceiverService

Приложение-получатель ДОЛЖНО объявить реализованный AbstractReceiverService в своем файле манифеста, добавить фильтр намерений с действием android.car.intent.action.RECEIVER_SERVICE для этой службы и потребовать разрешение 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() . Как поясняется в разделе (Sender) Request Connection , 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 конечной точке получателя с идентификатором FragmentB , он может использовать 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);
}

Как только AbstractReceiverService в клиенте-получателе отправляет Payload в конечную точку получателя, будет вызван связанный PayloadCallback .

Клиентское приложение МОЖЕТ зарегистрировать несколько конечных точек-получателей, если их receiverEndpointId уникальны среди клиентского приложения. receiverEndpointId будет использоваться AbstractReceiverService , чтобы решить, в какие конечные точки получателя отправлять полезную нагрузку. Например:

  • Отправитель указывает receiver_endpoint_id:FragmentB в Payload . При получении Payload AbstractReceiverService в получателе вызывает forwardPayload("FragmentB", payload) для отправки полезных данных в FragmentB
  • Отправитель указывает data_type:VOLUME_CONTROL в Payload . При получении 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. Чтобы выгрузить внутреннее состояние CarRemoteDeviceService и CarOccupantConnectionService :

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

Null CarRemoteDeviceManager и CarOccupantConnectionManager

Ознакомьтесь с возможными основными причинами:

  1. Автосервис разбился. Как было показано ранее, два менеджера намеренно сбрасываются до null при сбое автосервиса. При перезапуске автосервиса для двух менеджеров устанавливаются ненулевые значения.

  2. Ни 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() Соединение не установлено.

Клиент1 может отправлять полезную нагрузку клиенту2, но не наоборот.

Соединение является односторонним. Чтобы установить двустороннее соединение, client1 и client2 ДОЛЖНЫ запросить соединение друг с другом, а затем получить одобрение.