Una app con privilegios del sistema puede usar la API de Multi-Display Communications AAOS se comunique con la misma aplicación (mismo nombre de paquete) que se ejecuta en un diferente de ocupantes en un automóvil. En esta página, se describe cómo integrar la API. Para aprender más, también puedes ver CarOccupantZoneManager.OccupantZoneInfo.
Zona de ocupación
El concepto de zona de ocupación asigna a un usuario a un conjunto de pantallas. Cada la zona de ocupación tiene una pantalla con el tipo DISPLAY_TYPE_MAIN: Una zona de ocupación también puede tener pantallas adicionales, como una de clúster. A cada zona de ocupación se le asigna un usuario de Android. Cada usuario tiene su propia cuenta y aplicaciones.
Configuración de hardware
La API de comunicaciones admite un solo SoC. En el modelo único de SoC, todos los ocupantes zonas y usuarios se ejecutan en el mismo SoC. La API de Comms tiene tres componentes:
La API de Power Management permite al cliente administrar la potencia de la en las zonas de ocupantes.
La API de Discovery permite que el cliente supervise los estados de otro inquilino zonas del auto y supervisar clientes similares en esas zonas de ocupantes. Usa la API de Discovery antes de usar la API de Connection.
La API de conexión permite que el cliente se conecte con su cliente de intercambio de tráfico en otra zona de ocupación y enviar una carga útil al cliente de intercambio de tráfico.
Se requieren la API de Discovery y la de Connection para establecer la conexión. El poder Management de Google es opcional.
La API de Comms no admite la comunicación entre apps diferentes. En cambio, Está diseñado solo para la comunicación entre apps con el mismo nombre de paquete. y se usan solo para la comunicación entre diferentes usuarios visibles.
Guía de integración
Cómo implementar AbstractReceiverService
Para recibir el Payload
, la app receptora DEBE implementar los métodos abstractos.
definido 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) {
}
}
onConnectionInitiated()
se invoca cuando el cliente remitente solicita un
con este cliente receptor. Si se necesita la confirmación del usuario para establecer
la conexión, MyReceiverService
puede anular este método para iniciar un
actividad de permisos y llama a acceptConnection()
o rejectConnection()
,
en el resultado. De lo contrario, MyReceiverService
puede simplemente llamar
acceptConnection()
.`
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService puede anular esto
método para hacer lo siguiente:
- Reenvía el
Payload
a los extremos del receptor correspondientes, si los hay. Para obtener los extremos del receptor registrados y llamar agetAllReceiverEndpoints()
Para reenviar elPayload
a un extremo del receptor determinado, llamar aforwardPayload()
,
O BIEN:
- Almacenar en caché el
Payload
y enviarlo cuando el extremo del receptor esperado registrada, para la cual elMyReceiverService
se notifica a través deonReceiverRegistered()
Cómo declarar AbstractReceiverService
La app receptora DEBE declarar el AbstractReceiverService
implementado en su
archivo de manifiesto, agrega un filtro de intents con acciones
android.car.intent.action.RECEIVER_SERVICE
para este servicio y requieren la
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, otra app podría vincularse a este
y envíale un Payload
de forma directa.
Declara el permiso
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
otorgadas previamente por los archivos
de la lista de entidades permitidas. Por ejemplo, este es el archivo de la lista de entidades permitidas de
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>
Obtener administradores de Car
Para usar la API, la app cliente DEBE registrar un CarServiceLifecycleListener
en
Consultar los administradores de automóviles 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);
(Remitente) Discover
Antes de conectarse con el cliente receptor, el cliente remitente DEBE descubrir la
mediante el registro de 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 con el receptor, el remitente DEBE asegurarse de que todos se establecen las marcas de la zona de ocupación del receptor y la app del receptor. De lo contrario, se pueden producir 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 con el destinatario solo cuando se hayan del receptor. Dicho esto, hay excepciones:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
yFLAG_CLIENT_INSTALLED
son las 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 del conexión,
FLAG_OCCUPANT_ZONE_POWER_ON
y LosFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
se convierten en requisitos adicionales. Para un una mejor experiencia del usuario,FLAG_CLIENT_RUNNING
y También se recomiendanFLAG_CLIENT_IN_FOREGROUND
; de lo contrario, el usuario podría sorprenderte.Por ahora (Android 15), no se implementó
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
. La app cliente puede ignorarlo.Por ahora (Android 15), la API de Comms solo admite varios usuarios en el mismo Instancia de Android para que las apps similares puedan tener el mismo código de versión largo (
FLAG_CLIENT_SAME_LONG_VERSION
) y firma (FLAG_CLIENT_SAME_SIGNATURE
) Como resultado, las apps no necesitan verificar que el dos valores coinciden.
Para una mejor experiencia del usuario, el cliente remitente PUEDE mostrar una IU si no hay una marca.
automático. Por ejemplo, si no estableces FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
, el remitente
Puede mostrar un aviso o un diálogo para solicitarle al usuario que desbloquee la pantalla del
zona de ocupación del receptor.
Cuando el remitente ya no necesita detectar a los destinatarios (por ejemplo, cuando encuentra todos los receptores y las conexiones establecidas, o se vuelve inactiva), PUEDE detener el descubrimiento.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Cuando se detiene el descubrimiento, las conexiones existentes no se ven afectadas. El remitente puede
continúa enviando Payload
a los receptores conectados.
(Remitente) Solicitar conexión
Cuando se configuran 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);
}
Acepta la conexión (servicio del receptor)
Una vez que el remitente solicita una conexión con el receptor,
AbstractReceiverService
en la app receptora estará vinculado por el servicio del vehículo.
y se invocará AbstractReceiverService.onConnectionInitiated()
. Como
se explica en Solicitud de conexión(remitente).
onConnectionInitiated()
es un método abstracto y DEBE implementarlo
app cliente.
Cuando el destinatario acepta la solicitud de conexión,
Se invocará a ConnectionRequestCallback.onConnected()
y, luego, la conexión
de seguridad de la nube.
(Remitente) Envía la carga útil
Una vez que se establece 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 array de bytes en el Payload
. Si el botón
el remitente necesita enviar otros tipos de datos, DEBE serializar los datos en un byte
usar el array de bytes para construir un objeto Payload
y enviar el
Payload
Luego, el cliente receptor obtiene el array de bytes
Payload
y deserializa el array de bytes en el objeto de datos esperado.
Por ejemplo, si el remitente quiere enviar una hello
de cadena al receptor
con el ID FragmentB
, puede usar búferes proto para definir un tipo de datos
así:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
En la figura 1, se ilustra el flujo Payload
:
(Servicio del receptor) Recibe y envía la carga útil
Una vez que la app receptora recibe el Payload
, su
Se invocará AbstractReceiverService.onPayloadReceived()
. Como se explica en
Send thePayload, onPayloadReceived()
es un
abstraído y la app cliente DEBE implementarlo. En este método,
el cliente PUEDE reenviar el Payload
a los extremos del receptor correspondientes
almacenar en caché el Payload
y, luego, enviarlo una vez que el extremo del receptor esperado
registrada.
Registrar y cancelar el registro (extremo del receptor)
La app receptora DEBE llamar a registerReceiver()
para registrar el receptor
en los extremos. Un caso de uso típico es que un fragmento necesita recibir Payload
, de modo que
registra el extremo del receptor:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Una vez que AbstractReceiverService
en el cliente del receptor despache el
Payload
al extremo del receptor, se realizará el siguiente PayloadCallback
:
se invocan.
La app cliente PUEDE registrar múltiples extremos del receptor siempre y cuando su
Los elementos receiverEndpointId
son únicos entre la app cliente. El receiverEndpointId
AbstractReceiverService
para decidir qué receptor
extremos a los que enviar la carga útil. Por ejemplo:
- El remitente especifica
receiver_endpoint_id:FragmentB
en elPayload
. Cuándo cuando recibe elPayload
, elAbstractReceiverService
en las llamadas al receptorforwardPayload("FragmentB", payload)
para despachar la carga útil alFragmentB
- El remitente especifica
data_type:VOLUME_CONTROL
en elPayload
. Cuándo cuando recibe elPayload
, elAbstractReceiverService
en el receptor sabe que este tipo dePayload
debe enviarse aFragmentB
, de modo que llame aforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Remitente) Finaliza la conexión
Una vez que el remitente ya no necesita enviar Payload
al destinatario (por ejemplo,
se vuelve inactiva), DEBE finalizar la conexión.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Una vez desconectado, el remitente ya no podrá 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 los registros:
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
adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
CarRemoteDeviceManager y CarOccupantConnectionManager nulos
Echa un vistazo a estas posibles causas raíz:
El servicio de reparación se produjo un accidente. Como se ilustró anteriormente, los dos gerentes están Se restableció intencionalmente a
null
cuando el servicio de reparación del vehículo falle. Cuando se hace el servicio de reparación de automóviles se reinicia, los dos administradores se configuran con valores no nulos.Ni
CarRemoteDeviceService
niCarOccupantConnectionService
habilitado. Para determinar si uno de los dos está habilitado, ejecuta el siguiente comando: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 multipantalla, 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 producirse una excepción. En este caso, la app cliente puede verificar el mensaje en la excepción y el 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()
No se registróStateCallback
con esta selección InstanciaCarRemoteDeviceManager
.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 hay conexión establecida.disconnect()
No hay conexión establecida.
El cliente1 puede enviar la carga útil al cliente2, pero no al revés.
El diseño de la conexión es unidireccional. Para establecer una conexión bidireccional, tanto
client1
y client2
DEBEN solicitar una conexión entre sí y, luego,
y obtener aprobación.