API de Multi-Display Communications

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 whenMyReceiverServicehas received aPayloadfrom 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 a getAllReceiverEndpoints() Para reenviar el Payload a un extremo del receptor determinado, llamar a forwardPayload(),

O BIEN:

  • Almacenar en caché el Payload y enviarlo cuando el extremo del receptor esperado registrada, para la cual el MyReceiverService se notifica a través de onReceiverRegistered()

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 y FLAG_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 Los FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED se convierten en requisitos adicionales. Para un una mejor experiencia del usuario, FLAG_CLIENT_RUNNING y También se recomiendan FLAG_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:

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, 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 el Payload. Cuándo cuando recibe el Payload, el AbstractReceiverService en las llamadas al receptor forwardPayload("FragmentB", payload) para despachar la carga útil al FragmentB
  • El remitente especifica data_type:VOLUME_CONTROL en el Payload. Cuándo cuando recibe el Payload, el AbstractReceiverService en el receptor sabe que este tipo de Payload debe enviarse a FragmentB, de modo que llame a forwardPayload("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.

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 los registros:

    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

    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:

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

  2. Ni CarRemoteDeviceService ni CarOccupantConnectionService 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 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 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ó un StateCallback.
  • unregisterStateCallback() No se registró StateCallback con esta selección Instancia 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 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.