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 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 whenMyReceiverServicehas received aCarga ú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 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 el que se notifica a MyReceiverService a través de onReceiverRegistered().

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 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 de 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 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:

Envía la carga útil

Figura 1: Envía la carga útil.

(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 en Payload. Cuando se recibe 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 recibe el Payload, el AbstractReceiverService en el 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");
}

(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.

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. 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.

  2. No se habilita CarRemoteDeviceService o CarOccupantConnectionService. Para determinar si uno o el otro está habilitado, ejecuta lo siguiente:

    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 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ó un StateCallback.
  • unregisterStateCallback() Esta instancia de CarRemoteDeviceManager no registró ningún StateCallback.
  • 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.