Una app con privilegios del sistema en AAOS puede usar la API de Multi-Display Communications para comunicarse con la misma app (mismo nombre de paquete) que se ejecuta en una zona de ocupantes diferente en un automóvil. En esta página, se describe cómo integrar la API. Para obtener más información, también puedes consultar CarOccupantZoneManager.OccupantZoneInfo.
Zona de ocupación
El concepto de zona de ocupación asigna a un usuario a un conjunto de pantallas. Cada zona de ocupación tiene una pantalla con el tipo DISPLAY_TYPE_MAIN. Una zona para ocupantes también puede tener pantallas adicionales, como una pantalla de clúster. A cada zona de ocupante se le asigna un usuario de Android. Cada usuario tiene sus propias cuentas y apps.
Configuración de hardware
La API de Comms solo admite un SoC. En el modelo de SoC único, todas las zonas de ocupantes y los usuarios se ejecutan en el mismo SoC. La API de Comms consta de tres componentes:
La API de administración de energía permite que el cliente administre la energía de las pantallas en las zonas de ocupación.
La API de Discovery permite que el cliente supervise los estados de otras zonas de ocupantes en el vehículo y supervise a los clientes pares en esas zonas. Usa la API de Discovery antes de usar la API de Connection.
La API de Connection permite que el cliente se conecte a su cliente equivalente en otra zona de ocupantes y le envíe una carga útil.
La API de Discovery y la API de Connection son necesarias para la conexión. La API de Power Management es opcional.
La API de Comms no admite la comunicación entre diferentes apps. En cambio, está diseñado solo para la comunicación entre apps con el mismo nombre de paquete y se usa solo para la comunicación entre diferentes usuarios visibles.
Guía de integración
Implementa AbstractReceiverService
Para recibir el Payload
, la app receptora DEBE implementar los métodos abstractos
definidos en AbstractReceiverService
. Por ejemplo:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
Se invoca onConnectionInitiated()
cuando el cliente remitente solicita una conexión a este cliente receptor. Si se necesita la confirmación del usuario para establecer la conexión, MyReceiverService
puede anular este método para iniciar una actividad de permiso y llamar a acceptConnection()
o rejectConnection()
según el resultado. De lo contrario, MyReceiverService
puede llamar a acceptConnection()
.
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Carga útilfrom the sender client.
MyReceiverService` puede anular este método para lo siguiente:
- Reenvía el
Payload
a los extremos del receptor correspondientes, si los hay. Para obtener los extremos del receptor registrados, llama agetAllReceiverEndpoints()
. Para reenviar elPayload
a un extremo receptor determinado, llama aforwardPayload()
.
O
- Almacena en caché el
Payload
y envíalo cuando se registre el extremo del receptor esperado, para el que se notifica aMyReceiverService
a través deonReceiverRegistered()
.
Declara AbstractReceiverService
La app del receptor DEBE declarar el AbstractReceiverService
implementado en su archivo de manifiesto, agregar un filtro de intents con la acción android.car.intent.action.RECEIVER_SERVICE
para este servicio y requerir el permiso 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>
El permiso android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE
garantiza que solo el framework pueda vincularse a este servicio. Si este servicio
no requiere el permiso, es posible que una app diferente pueda vincularse a este
servicio y enviarle un Payload
directamente.
Cómo declarar permisos
La app cliente DEBE declarar los permisos en su archivo de manifiesto.
<!-- 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"/>
Cada uno de los tres permisos anteriores son permisos con privilegios, que DEBEN otorgarse previamente con archivos de lista de entidades permitidas. Por ejemplo, este es el archivo de la lista de entidades permitidas de la app de 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>
Cómo obtener administradores de vehículos
Para usar la API, la app cliente DEBE registrar un CarServiceLifecycleListener
para obtener los administradores de vehículos asociados:
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);
Descubrimiento (Remitente)
Antes de conectarse al cliente receptor, el cliente remitente DEBE descubrir al cliente receptor registrando un 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);
}
Antes de solicitar una conexión al receptor, el remitente DEBE asegurarse de que se hayan configurado todas las marcas de la zona de ocupantes y la app del receptor. De lo contrario, pueden producirse errores. Por ejemplo:
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;
}
Recomendamos que el remitente solicite una conexión al receptor solo cuando se hayan configurado todas las marcas del receptor. Dicho esto, hay excepciones:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
yFLAG_CLIENT_INSTALLED
son los requisitos mínimos necesarios para establecer una conexión.Si la app receptora necesita mostrar una IU para obtener la aprobación del usuario de la conexión,
FLAG_OCCUPANT_ZONE_POWER_ON
yFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
se convierten en requisitos adicionales. Para una mejor experiencia del usuario, también se recomiendanFLAG_CLIENT_RUNNING
yFLAG_CLIENT_IN_FOREGROUND
. De lo contrario, el usuario podría sorprenderse.Por ahora (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
no está implementado. La app cliente puede ignorarlo.Por ahora (Android 15), la API de Comms solo admite varios usuarios en la misma instancia de Android para que las apps de pares puedan tener el mismo código de versión largo (
FLAG_CLIENT_SAME_LONG_VERSION
) y la misma firma (FLAG_CLIENT_SAME_SIGNATURE
). Como resultado, las apps no necesitan verificar que los dos valores coincidan.
Para brindar una mejor experiencia del usuario, el cliente del remitente PUEDE mostrar una IU si no se establece una marca. Por ejemplo, si no se establece FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
, el remitente puede mostrar un aviso o un diálogo para solicitarle al usuario que desbloquee la pantalla de la zona del ocupante del receptor.
Cuando el remitente ya no necesite descubrir los receptores (por ejemplo, cuando encuentre todos los receptores y las conexiones establecidas o se vuelva inactivo), PUEDE detener el descubrimiento.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Cuando se detiene el descubrimiento, las conexiones existentes no se ven afectadas. El remitente puede seguir enviando Payload
a los receptores conectados.
Solicitar conexión (remitente)
Cuando se establecen todas las marcas del receptor, el remitente PUEDE solicitar una conexión al receptor:
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);
}
(Servicio del receptor) Acepta la conexión
Una vez que el remitente solicite una conexión al receptor, el servicio de automóvil vinculará AbstractReceiverService
en la app del receptor y se invocará AbstractReceiverService.onConnectionInitiated()
. Como se explica en Solicita conexión(emisor), onConnectionInitiated()
es un método abstracto y la app cliente DEBE implementarlo.
Cuando el receptor acepta la solicitud de conexión, se invoca el ConnectionRequestCallback.onConnected()
del remitente y, luego, se establece la conexión.
(Remitente) Envía la carga útil
Una vez establecida la conexión, el remitente PUEDE enviar Payload
al receptor:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
El remitente puede colocar un objeto Binder
o un arreglo de bytes en el Payload
. Si el remitente necesita enviar otros tipos de datos, DEBE serializar los datos en un array de bytes, usar el array de bytes para construir un objeto Payload
y enviar el Payload
. Luego, el cliente receptor obtiene el array de bytes del Payload
recibido y lo deserializa en el objeto de datos esperado.
Por ejemplo, si el remitente quiere enviar una cadena hello
al extremo del receptor con el ID FragmentB
, puede usar Proto Buffers para definir un tipo de datos de la siguiente manera:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
En la Figura 1, se ilustra el flujo de Payload
:
(Servicio del destinatario) Recibir y enviar la carga útil
Una vez que la app receptora reciba el Payload
, se invocará su AbstractReceiverService.onPayloadReceived()
. Como se explica en Cómo enviar la carga útil, onPayloadReceived()
es un método abstracto que la app cliente DEBE implementar. En este método, el cliente PUEDE reenviar el Payload
a los extremos del receptor correspondientes, o bien almacenar en caché el Payload
y, luego, enviarlo una vez que se haya registrado el extremo del receptor esperado.
(Extremo del receptor) Registro y anulación de registro
La app del receptor DEBE llamar a registerReceiver()
para registrar los extremos del receptor. Un caso de uso típico es que un fragmento necesita recibir Payload
, por lo que registra un extremo de receptor:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Una vez que el AbstractReceiverService
en el cliente receptor envíe el Payload
al extremo receptor, se invocará el PayloadCallback
asociado.
La app cliente PUEDE registrar varios extremos de receptor, siempre que sus receiverEndpointId
sean únicos entre la app cliente. AbstractReceiverService
usará el receiverEndpointId
para decidir a qué extremos de receptor enviar la carga útil. Por ejemplo:
- El remitente especifica
receiver_endpoint_id:FragmentB
enPayload
. Cuando se recibePayload
, elAbstractReceiverService
en el receptor llama aforwardPayload("FragmentB", payload)
para enviar la carga útil aFragmentB
. - El remitente especifica
data_type:VOLUME_CONTROL
enPayload
. Cuando recibe elPayload
, elAbstractReceiverService
en el receptor sabe que este tipo dePayload
se debe enviar aFragmentB
, por lo que llama aforwardPayload("FragmentB", payload)
.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Receptor) Finaliza la conexión
Una vez que el remitente ya no necesite enviar Payload
al destinatario (por ejemplo,
se vuelve inactivo), DEBE finalizar la conexión.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Una vez que se desconecta, el remitente ya no puede enviar Payload
al receptor.
Flujo de conexión
En la Figura 2, se ilustra un flujo de conexión.
Solución de problemas
Verifica los registros
Para verificar los registros correspondientes, haz lo siguiente:
Ejecuta este comando para registrar:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
Para volcar el estado interno de
CarRemoteDeviceService
yCarOccupantConnectionService
, haz lo siguiente:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
CarRemoteDeviceManager y CarOccupantConnectionManager nulos
Consulta estas posibles causas raíz:
Se produjo una falla en el servicio del automóvil. Como se ilustró anteriormente, los dos administradores se restablecen de forma intencional a
null
cuando falla el servicio del automóvil. Cuando se reinicia el servicio de automóviles, los dos administradores se establecen en valores no nulos.No se habilita
CarRemoteDeviceService
oCarOccupantConnectionService
. Para determinar si uno o el otro está habilitado, ejecuta lo siguiente:adb shell dumpsys car_service --services CarFeatureController
Busca
mDefaultEnabledFeaturesFromConfig
, que debería contenercar_remote_device_service
ycar_occupant_connection_service
. Por ejemplo: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]
De forma predeterminada, estos dos servicios están inhabilitados. Cuando un dispositivo admite varias pantallas, DEBES superponer este archivo de configuración. Puedes habilitar los dos servicios en un archivo de configuración:
// 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>
Excepción cuando se llama a la API
Si la app cliente no usa la API según lo previsto, puede ocurrir una excepción. En este caso, la app cliente puede verificar el mensaje en la excepción y la pila de fallas para resolver el problema. Estos son algunos ejemplos de uso inadecuado de la API:
registerStateCallback()
Este cliente ya registró unStateCallback
.unregisterStateCallback()
Esta instancia deCarRemoteDeviceManager
no registró ningúnStateCallback
.registerReceiver()
receiverEndpointId
ya está registrado.unregisterReceiver()
receiverEndpointId
no está registrado.requestConnection()
Ya existe una conexión pendiente o establecida.cancelConnection()
No hay ninguna conexión pendiente para cancelar.sendPayload()
No se estableció conexión.disconnect()
No se estableció una conexión.
El cliente 1 puede enviar la carga útil al cliente 2, pero no al revés.
La conexión es unidireccional de forma predeterminada. Para establecer una conexión de dos vías, client1
y client2
DEBEN solicitar una conexión entre sí y, luego, obtener la aprobación.