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

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

Зона проживания

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

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

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

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

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

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

Для подключения требуются Discovery API и Connection API. Power management 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 и десериализует массив байтов в ожидаемый объект данных. Например, если отправитель хочет отправить String hello в конечную точку получателя с идентификатором FragmentB , он может использовать Proto Buffers для определения типа данных следующим образом:

message MyData {
  required string receiver_endpoint_id = 1;
  required string data = 2;
}

Рисунок 1 иллюстрирует поток Payload :

Отправить полезную нагрузку

Рисунок 1. Отправка полезной нагрузки.

(Услуга приемника) Прием и отправка полезной нагрузки

Как только приложение-получатель получит Payload , будет вызван его AbstractReceiverService.onPayloadReceived() . Как поясняется в разделе Отправка payload , 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 ДОЛЖНЫ запросить соединение друг с другом, а затем получить одобрение.

,

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

Зона проживания

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

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

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

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

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

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

Для подключения требуются Discovery API и Connection API. Power management 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 и десериализует массив байтов в ожидаемый объект данных. Например, если отправитель хочет отправить String hello в конечную точку получателя с идентификатором FragmentB , он может использовать Proto Buffers для определения типа данных следующим образом:

message MyData {
  required string receiver_endpoint_id = 1;
  required string data = 2;
}

Рисунок 1 иллюстрирует поток Payload :

Отправить полезную нагрузку

Рисунок 1. Отправка полезной нагрузки.

(Услуга приемника) Прием и отправка полезной нагрузки

Как только приложение-получатель получит Payload , будет вызван его AbstractReceiverService.onPayloadReceived() . Как поясняется в разделе Отправка payload , 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 ДОЛЖНЫ запросить соединение друг с другом, а затем получить одобрение.