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 :
(Приёмная служба) Приём и отправка полезной нагрузки
Как только принимающее приложение получит 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. При полученииPayloadAbstractReceiverServiceв получателе знает, что этот типPayloadдолжен быть отправленFragmentB, поэтому он вызываетforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Отправитель) Разъединить соединение
Как только отправитель перестаёт нуждаться в передаче Payload получателю (например, становится неактивным), он ДОЛЖЕН разорвать соединение.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
После разрыва соединения отправитель больше не может отправлять Payload получателю.
Поток соединений
Схема подключения показана на рисунке 2.
Поиск неисправностей
Проверьте журналы.
Чтобы проверить соответствующие журналы:
Выполните эту команду для ведения журнала:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"Чтобы вывести внутреннее состояние служб
CarRemoteDeviceServiceиCarOccupantConnectionService:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Значения CarRemoteDeviceManager и CarOccupantConnectionManager равны нулю.
Рассмотрим возможные первопричины:
Сервис такси вышел из строя. Как было показано ранее, при сбое сервиса такси оба менеджера намеренно сбрасываются до
nullзначений. При перезапуске сервиса такси обоим менеджерам присваиваются ненулевые значения.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 ДОЛЖНЫ запросить соединение друг у друга, а затем получить подтверждение.