API Multi-Display Communications

L'API Multi-Display Communications può essere utilizzata da un'app con privilegi di sistema in AAOS per comunicare con la stessa app (stesso nome di pacchetto) in esecuzione in un'altra zona degli occupanti di un'auto. Questa pagina descrive come integrare l'API. Per scoprire di più, puoi anche consultare CarOccupantZoneManager.OccupantZoneInfo.

Zona degli occupanti

Il concetto di zona di presenza mappa un utente a un insieme di display. Ogni zona occupante ha un display di tipo DISPLAY_TYPE_MAIN. Una zona per gli occupanti può avere anche display aggiuntivi, ad esempio un display cluster. A ogni zona occupante viene assegnato un utente Android. Ogni utente ha i propri account e le proprie app.

Configurazione hardware

L'API Comms supporta un solo SoC. Nel modello con un solo SoC, tutte le zone e gli utenti occupanti vengono eseguiti sullo stesso SoC. L'API Comms è composta da tre componenti:

  • L'API di gestione dell'alimentazione consente al client di gestire l'alimentazione dei display nelle zone per i passeggeri.

  • L'API Discovery consente al client di monitorare gli stati delle altre zone degli occupanti dell'auto e di monitorare i client peer in queste zone. Utilizza l'API Discovery prima di utilizzare l'API Connection.

  • L'API Connection consente al client di connettersi al client peer in un'altra zona di occupanti e di inviare un payload al client peer.

Per la connessione sono necessarie l'API Discovery e l'API Connection. L'API Power Management è facoltativa.

L'API Comms non supporta la comunicazione tra app diverse. È invece progettato solo per la comunicazione tra app con lo stesso nome del pacchetto e utilizzato solo per la comunicazione tra utenti visibili diversi.

Guida all'integrazione

Implementa AbstractReceiverService

Per ricevere il Payload, l'app di destinazione DEVE implementare i metodi astratti definiti in AbstractReceiverService. Ad esempio:

public class MyReceiverService extends AbstractReceiverService {

    @Override
    public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
    }

    @Override
    public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
            @NonNull Payload payload) {
    }
}

onConnectionInitiated() viene invocato quando il client mittente richiede una connessione a questo client destinatario. Se per stabilire la connessione è necessaria la conferma dell'utente, MyReceiverService può eseguire l'override di questo metodo per avviare un'attività di autorizzazione e chiamare acceptConnection() o rejectConnection() in base al risultato. In caso contrario, MyReceiverService può semplicemente chiamareacceptConnection().`

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` può eseguire l'override di questo metodo per:

  • Inoltra Payload agli endpoint di destinazione corrispondenti, se presenti. Per recuperare gli endpoint del ricevitore registrati, chiama getAllReceiverEndpoints(). Per inoltrare il Payload a un determinato endpoint di ricezione, chiama forwardPayload()

OPPURE

  • Memorizza nella cache il Payload e invialo quando l'endpoint del destinatario previsto è registrato, per il quale il Payload viene informato tramite onReceiverRegistered()MyReceiverService

Dichiara AbstractReceiverService

L'app di ricezione DEVE dichiarare il AbstractReceiverService implementato nel suo file manifest, aggiungere un filtro per intent con l'azione android.car.intent.action.RECEIVER_SERVICE per questo servizio e richiedere l'autorizzazione 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>

L'autorizzazione android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE garantisce che solo il framework possa eseguire il binding a questo servizio. Se questo servizio non richiede l'autorizzazione, un'app diversa potrebbe essere in grado di eseguire il binding a questo servizio e inviargli direttamente un Payload.

Dichiarare l'autorizzazione

L'app client DEVE dichiarare le autorizzazioni nel file manifest.

<!-- 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"/>

Ognuna delle tre autorizzazioni sopra indicate è un'autorizzazione con privilegi, che DEVE essere pre-conceduta dai file della lista consentita. Ad esempio, ecco il file della lista consentita dell'app 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>

Gestire gli amministratori dell'auto

Per utilizzare l'API, l'app client DEVE registrare un CarServiceLifecycleListener per recuperare i gestori dell'auto associati:

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);

(Mittente) Discover

Prima di connettersi al client di ricezione, il client di invio DEVE rilevare il client di ricezione 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);
}

Prima di richiedere una connessione al destinatario, il mittente DEVE assicurarsi che tutti i flag della zona occupante e dell'app del destinatario siano impostati. In caso contrario, possono verificarsi errori. Ad esempio:

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;
}

Consigliamo al mittente di richiedere una connessione al destinatario solo quando sono impostati tutti i flag del destinatario. Detto questo, esistono delle eccezioni:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READY e FLAG_CLIENT_INSTALLED sono i requisiti minimi necessari per stabilire una connessione.

  • Se l'app di destinazione deve mostrare un'interfaccia utente per ottenere l'approvazione dell'utente per la connessione, FLAG_OCCUPANT_ZONE_POWER_ON e FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED diventano requisiti aggiuntivi. Per un'esperienza utente migliore, sono consigliati anche FLAG_CLIENT_RUNNING e FLAG_CLIENT_IN_FOREGROUND, altrimenti l'utente potrebbe essere sorpreso.

  • Per il momento (Android 15), FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED non è implementato. L'app client può semplicemente ignorarla.

  • Per il momento (Android 15), l'API Comms supporta solo più utenti nella stessa istanza Android, in modo che le app peer possano avere lo stesso codice di versione lungo (FLAG_CLIENT_SAME_LONG_VERSION) e la stessa firma (FLAG_CLIENT_SAME_SIGNATURE). Di conseguenza, le app non devono verificare che i due valori siano uguali.

Per una migliore esperienza utente, il client mittente PUÒ mostrare un'interfaccia utente se non è impostato un flag. Ad esempio, se FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED non è impostato, il mittente può mostrare un messaggio popup o una finestra di dialogo per chiedere all'utente di sbloccare lo schermo della zona occupante del destinatario.

Quando il mittente non ha più bisogno di rilevare i destinatari (ad esempio, quando trova tutti i destinatari e le connessioni stabilite o diventa inattivo), PUÒ interrompere il rilevamento.

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

Quando la scoperta viene interrotta, le connessioni esistenti non sono interessate. Il mittente può continuare a inviare Payload ai destinatari collegati.

(Mittente) Richiedi connessione

Quando tutti i flag del ricevitore sono impostati, il mittente PUÒ richiedere una connessione al ricevitore:

    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);
}

(Servizio di ricezione) Accetta la connessione

Una volta che il mittente richiede una connessione al destinatario, il AbstractReceiverService nell'app del destinatario verrà vincolato dal servizio auto e verrà invocato AbstractReceiverService.onConnectionInitiated(). Come spiegato in (Sender) Request Connection,onConnectionInitiated() è un metodo astratto e DEVE essere implementato dall'app client.

Quando il destinatario accetta la richiesta di connessione, viene invocato il ConnectionRequestCallback.onConnected() del mittente e la connessione viene stabilita.

(Mittente) Invia il payload

Una volta stabilita la connessione, il mittente PUÒ inviare Payload al destinatario:

if (mOccupantConnectionManager != null) {
    Payload payload = ...;
    try {
        mOccupantConnectionManager.sendPayload(receiverZone, payload);
    } catch (CarOccupantConnectionManager.PayloadTransferException e) {
        Log.e(TAG, "Failed to send Payload to " + receiverZone);
    }
}

Il mittente può inserire un oggetto Binder o un array di byte in Payload. Se il mittente deve inviare altri tipi di dati, DEVE serializzare i dati in un array di byte, utilizzare l'array di byte per creare un oggetto Payload e inviare il Payload. Il client di ricezione recupera l'array di byte dal messaggio ricevutoPayload e lo deserializza nell'oggetto dati previsto. Ad esempio, se il mittente vuole inviare una stringa hello all'endpoint del destinatario con ID FragmentB, può utilizzare Proto Buffers per definire un tipo di dato come questo:

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

La Figura 1 illustra il flusso Payload:

Invia il payload

Figura 1. Invia il payload.

(Servizio di ricezione) Ricevi e invia il payload

Una volta che l'app di destinazione riceve il messaggio Payload, viene invocato il suo AbstractReceiverService.onPayloadReceived(). Come spiegato in Invio del payload, onPayloadReceived() è un metodo astratto e DEVE essere implementato dall'app client. In questo metodo, il client PUÒ inoltrare Payload agli endpoint di ricezione corrispondenti oppure memorizzare nella cache Payload e inviarlo una volta registrato l'endpoint di ricezione previsto.

(Endpoint del ricevitore) Registrazione e annullamento della registrazione

L'app di ricezione DEVE chiamare registerReceiver() per registrare gli endpoint di ricezione. Un caso d'uso tipico è che un frammento deve ricevere Payload, quindi registra un endpoint di ricezione:

private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
    …
};

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.registerReceiver("FragmentB",
                getActivity().getMainExecutor(), mPayloadCallback);
}

Una volta che AbstractReceiverService nel client di ricezione invia Payload all'endpoint di ricezione, verrà invocato PayloadCallback associato.

L'app client PUÒ registrare più endpoint di ricezione purché i relativi receiverEndpointId siano univoci nell'app client. Il receiverEndpointId verrà utilizzato dal AbstractReceiverService per decidere a quali endpoint di ricezione inviare il payload. Ad esempio:

  • Il mittente specifica receiver_endpoint_id:FragmentB in Payload. Al ricevere il Payload, il AbstractReceiverService nel ricevitore chiama forwardPayload("FragmentB", payload) per inviare il payload a FragmentB
  • Il mittente specifica data_type:VOLUME_CONTROL in Payload. Quando riceve il Payload, il AbstractReceiverService nel ricevitore sa che questo tipo di Payload deve essere inviato a FragmentB, quindi chiama forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Mittente) Termina la connessione

Quando il mittente non ha più bisogno di inviare Payload al destinatario (ad esempio, diventa inattivo), DOVREBBE terminare la connessione.

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.disconnect(receiverZone);
}

Una volta disconnesso, il mittente non può più inviare Payload al destinatario.

Flusso di connessione

Un flusso di connessione è illustrato nella Figura 2.

Flusso di connessione

Figura 2. Flusso di connessione.

Risoluzione dei problemi

Controlla i log

Per controllare i log corrispondenti:

  1. Esegui questo comando per la registrazione:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
    
  2. Per eseguire il dump dello stato interno di CarRemoteDeviceService e CarOccupantConnectionService:

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
    

CarRemoteDeviceManager e CarOccupantConnectionManager null

Controlla queste possibili cause principali:

  1. Il servizio di auto si è arrestato in modo anomalo. Come illustrato in precedenza, i due gestori vengono deliberatamente reimpostati su null quando il servizio per auto si arresta in modo anomalo. Quando il servizio auto viene riavviato, i due gestori vengono impostati su valori non null.

  2. CarRemoteDeviceService o CarOccupantConnectionService non è attivo. Per determinare se è abilitato uno o l'altro, esegui:

    adb shell dumpsys car_service --services CarFeatureController
    
    • Cerca mDefaultEnabledFeaturesFromConfig, che deve contenere car_remote_device_service e car_occupant_connection_service. Per esempio:

      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]
      
    • Per impostazione predefinita, questi due servizi sono disattivati. Quando un dispositivo supporta il multi-display, DEVI sovrapporre questo file di configurazione. Puoi attivare i due servizi in un file di configurazione:

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

Eccezione durante la chiamata all'API

Se l'app client non utilizza l'API come previsto, può verificarsi un'eccezione. In questo caso, l'app client può controllare il messaggio nell'eccezione e lo stack dell'arresto anomalo per risolvere il problema. Ecco alcuni esempi di utilizzo improprio dell'API:

  • registerStateCallback() Questo cliente ha già registrato un StateCallback.
  • unregisterStateCallback() Nessun StateCallback è stato registrato da questa CarRemoteDeviceManager istanza.
  • registerReceiver() receiverEndpointId è già registrato.
  • unregisterReceiver() receiverEndpointId non è registrato.
  • requestConnection() Esiste già una connessione in attesa o stabilita.
  • cancelConnection() Nessuna connessione in attesa da annullare.
  • sendPayload() Nessuna connessione stabilita.
  • disconnect() Nessuna connessione stabilita.

Il client1 può inviare il payload al client2, ma non viceversa

La connessione è unidirezionale per impostazione predefinita. Per stabilire una connessione bidirezionale, sia client1 sia client2 DEVONO richiedere una connessione tra loro e poi ottenere l'approvazione.