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

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

Зона пребывания

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

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

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

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

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

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

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

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

Руководство по интеграции

Реализовать 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 Comms поддерживает только несколько пользователей на одном устройстве 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() . Как поясняется в разделе «(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() для регистрации конечных точек приёмника. Типичный пример использования — когда фрагменту требуется получить 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 , ДОЛЖНЫ запросить соединение друг у друга и получить подтверждение.