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

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

Зона обитаемости

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

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

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

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

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

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

Для подключения необходимы API обнаружения и API подключения. API управления питанием является необязательным.

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() вызывается, когда MyReceiverService получает Payload от клиента-отправителя. 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) 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 в приложении получателя будет связан со службой car, и будет вызван метод 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 и десериализует его в ожидаемый объект данных. Например, если отправитель хочет отправить строку hello на конечную точку получателя с ID 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() для регистрации конечных точек приемника. Типичный пример использования: фрагменту необходимо получить 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) чтобы отправить 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

Значения 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 ДОЛЖНЫ запросить соединение друг у друга, а затем получить подтверждение.