API de Multi-Display Communications

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 ocupantes

El concepto de una zona de ocupante asigna un usuario a un conjunto de pantallas. Cada zona de ocupantes tiene una pantalla con el tipo DISPLAY_TYPE_MAIN. Una zona de ocupantes también puede tener pantallas adicionales, como una pantalla de clúster. A cada zona de ocupantes 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 único SoC. En el modelo de SoC único, todas las zonas y los usuarios del vehículo 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 los ocupantes.

  • La API de Discovery permite que el cliente supervise los estados de otras zonas de ocupantes en el automóvil y supervise los clientes similares en esas zonas de ocupantes. 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 par en otra zona de ocupante y que le envíe una carga útil.

Se requieren la API de Discovery y la API de Connection para la conexión. La API de administración de energía 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 emisor 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().

Se invoca onPayloadReceived() cuando MyReceiverService recibe un Payload del cliente remitente. MyReceiverService puede anular este método para hacer lo siguiente:

  • Reenvía el Payload a los extremos del receptor correspondientes, si los hay. Para obtener los extremos de receptor registrados, llama a getAllReceiverEndpoints(). Para reenviar el Payload a un extremo receptor determinado, llama a forwardPayload().

O

  • Almacena en caché el Payload y envíalo cuando se registre el extremo del receptor esperado, para lo cual se notifica al MyReceiverService a través de onReceiverRegistered().

Cómo declarar AbstractReceiverService

La app receptora DEBE declarar el objeto 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_SERVICEgarantiza que solo el framework pueda vincularse a este servicio. Si este servicio no requiere el permiso, es posible que otra app pueda vincularse a este servicio y enviarle un Payload directamente.

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 privilegiados, que DEBEN otorgarse previamente a través de 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>

Obtener administradores de automóviles

Para usar la API, la app cliente DEBE registrar un CarServiceLifecycleListener para obtener los administradores de Car 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);

Descubre (remitente)

Antes de conectarse al cliente receptor, el cliente emisor 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 emisor DEBE asegurarse de que estén establecidos todos los parámetros de la zona de ocupante del receptor y de 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 emisor solicite una conexión al receptor solo cuando se hayan establecido todas las marcas del receptor. Dicho esto, hay excepciones:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READY y FLAG_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 para la conexión, FLAG_OCCUPANT_ZONE_POWER_ON y FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED se convierten en requisitos adicionales. Para una mejor experiencia del usuario, también se recomiendan FLAG_CLIENT_RUNNING y FLAG_CLIENT_IN_FOREGROUND. De lo contrario, el usuario podría sorprenderse.

  • Por el momento (Android 15), FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED no está implementado. La app cliente puede ignorarlo.

  • Por el momento (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 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 FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED no está configurado, el remitente puede mostrar un mensaje emergente o un diálogo para solicitarle al usuario que desbloquee la pantalla de la zona de ocupante del receptor.

Cuando el remitente ya no necesita descubrir los receptores (por ejemplo, cuando encuentra todos los receptores y establece conexiones o se vuelve inactivo), PUEDE detener el descubrimiento.

if (mRemoteDeviceManager != null) {
    mRemoteDeviceManager.unregisterStateCallback();
}

Cuando se detiene el descubrimiento, las conexiones existentes no se ven afectadas. El emisor puede seguir enviando Payload a los receptores conectados.

(Remitente) Solicitar conexión

Cuando se configuran todas las marcas del receptor, el emisor 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 receptor) Acepta la conexión

Una vez que el remitente solicita una conexión con el receptor, el servicio de automóvil vinculará el AbstractReceiverService en la app del receptor y se invocará AbstractReceiverService.onConnectionInitiated(). Como se explica en (Sender) Request Connection, 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 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 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 desea enviar una cadena hello al extremo del receptor con el ID FragmentB, puede usar búferes de protocolo para definir un tipo de datos como este:

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

La figura 1 ilustra el flujo de Payload:

Envía la carga útil

Figura 1: Envía la carga útil.

(Servicio del receptor) Recibe y envía la carga útil

Una vez que la app receptora recibe el Payload, se invoca su AbstractReceiverService.onPayloadReceived(). Como se explica en Envía la carga útil, onPayloadReceived() es un método abstracto y la app cliente DEBE implementarlo. En este método, el cliente PUEDE reenviar el Payload a los extremos del receptor correspondientes o almacenar en caché el Payload y, luego, enviarlo una vez que se registre el extremo del receptor esperado.

(Extremo del receptor) Registro y cancelación del registro

La app receptora DEBE llamar a registerReceiver() para registrar los extremos del receptor. Un caso de uso típico es que un Fragment 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 del receptor envía el Payload al extremo del receptor, se invoca el PayloadCallback asociado.

La app cliente PUEDE registrar varios extremos de receptor, siempre y cuando sus receiverEndpointId sean únicos entre la app cliente. El receiverEndpointId será utilizado por el AbstractReceiverService para decidir a qué extremos de receptor se enviará la carga útil. Por ejemplo:

  • El remitente especifica receiver_endpoint_id:FragmentB en Payload. Cuando se recibe el Payload, el AbstractReceiverService en el receptor llama a forwardPayload("FragmentB", payload) para enviar la carga útil a FragmentB.
  • El remitente especifica data_type:VOLUME_CONTROL en Payload. Cuando se recibe el Payload, el AbstractReceiverService del receptor sabe que este tipo de Payload se debe enviar a FragmentB, por lo que llama a forwardPayload("FragmentB", payload).
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Emisor) Finaliza la conexión

Una vez que el remitente ya no necesite enviar Payload al receptor (por ejemplo, si se vuelve inactivo), 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.

Flujo de conexión

Figura 2: Flujo de conexión.

Solución de problemas

Verifica los registros

Para verificar los registros correspondientes, haz lo siguiente:

  1. 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"
  2. Para volcar el estado interno de CarRemoteDeviceService y CarOccupantConnectionService, 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:

  1. El servicio de automóvil falló. Como se ilustró anteriormente, los dos administradores se restablecen intencionalmente a null cuando falla el servicio del automóvil. Cuando se reinicia el servicio de automóvil, los dos administradores se establecen en valores no nulos.

  2. CarRemoteDeviceService o CarOccupantConnectionService no están habilitados. Para determinar si uno u otro está habilitado, ejecuta el siguiente comando:

    adb shell dumpsys car_service --services CarFeatureController
    • Busca mDefaultEnabledFeaturesFromConfig, que debería contener car_remote_device_service y car_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 producirse una excepción. En este caso, la app cliente puede verificar el mensaje en la excepción y el registro de pila de fallas para resolver el problema. Estos son algunos ejemplos de uso inadecuado de la API:

  • registerStateCallback() Este cliente ya registró un StateCallback.
  • unregisterStateCallback() No se registró ningún StateCallback en esta instancia de CarRemoteDeviceManager.
  • 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ó conexión.

El cliente1 puede enviar una carga útil al cliente2, pero no al revés.

La conexión es unidireccional por diseño. Para establecer una conexión bidireccional, tanto client1 como client2 DEBEN solicitar una conexión entre sí y, luego, obtener la aprobación.