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 della zona degli occupanti di un'auto. In questa pagina viene descritto come integrare l'API. Per ulteriori informazioni puoi anche vedere CarOccupantZoneManager.OccupantZoneInfo.

Zona occupante

Il concetto di zona di persone mappa l'utente a una serie di display. Ciascuna ha un display con il tipo DISPLAY_TYPE_MAIN. Una zona degli occupanti può anche avere display aggiuntivi, ad esempio un display del cluster. A ogni zona degli occupanti viene assegnato un utente Android. Ogni utente ha i propri account e le app.

Configurazione hardware

L'API Comms supporta un solo SoC. Nel singolo modello SoC, di zone e utenti vengono eseguiti sullo stesso SoC. L'API Comms è costituita da tre componenti:

  • L'API Power Management consente al cliente di gestire la potenza del viene visualizzato nelle zone degli occupanti.

  • L'API Discovery consente al client di monitorare gli stati di un altro occupante le zone dell'auto e monitorare i client peer in quelle zone degli occupanti. Utilizza le funzionalità di l'API Discovery prima di utilizzare l'API Connection.

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

Per la connessione sono necessarie l'API Discovery e l'API Connection. The Power l'API di gestione dei dati è facoltativa.

L'API Comms non supporta la comunicazione tra app diverse. Invece, È stata progettata solo per la comunicazione tra app con lo stesso nome di pacchetto e utilizzati solo per le comunicazioni tra diversi utenti visibili.

Guida all'integrazione

Implementazione di AbstractReceiverService

Per ricevere Payload, l'app ricevente DEVE implementare metodi astratti. definita 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 richiamato quando il client del mittente richiede un la connessione a questo client ricevitore. Se è necessaria la conferma dell'utente per stabilire connessione, MyReceiverService può sostituire questo metodo per avviare un attività di autorizzazione e chiamare in base a acceptConnection() o rejectConnection() sul risultato. In caso contrario, MyReceiverService può semplicemente chiamare acceptConnection().

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService' può ignorare questo per:

  • Inoltra Payload agli endpoint ricevitori corrispondenti, se presenti. A recuperare gli endpoint del ricevitore registrati, chiamare getAllReceiverEndpoints(). A inoltra Payload a un determinato endpoint destinatario, chiama forwardPayload()

OPPURE,

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

Dichiara AbstractReceiverService

L'app ricevente DEVE dichiarare l'oggetto AbstractReceiverService implementato nel file manifest, aggiungi un filtro per intent con azione android.car.intent.action.RECEIVER_SERVICE per questo servizio e richiedono 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 assicura che solo il framework possa essere associato a questo servizio. Se questo servizio non richiede l'autorizzazione, un'altra app potrebbe essere in grado di associarsi a questo e inviare direttamente un Payload.

Dichiara 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-concessi da file della lista consentita. Ad esempio, ecco il file della lista consentita 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>

Coinvolgi i gestori dell'auto

Per utilizzare l'API, l'app client DEVE registrare un CarServiceLifecycleListener a 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);

Discover (Mittente)

Prima di connettersi al client destinatario, il client mittente DEVE scoprire il il client ricevente 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 degli occupanti del ricevitore e dell'app ricevitore sono impostati. Altrimenti, che 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 tutti i del ricevitore siano impostati. Tuttavia, ci sono delle eccezioni:

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

  • Se l'app ricevente deve visualizzare una UI per ottenere l'approvazione dell'utente connessione, FLAG_OCCUPANT_ZONE_POWER_ON e FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED diventano requisiti aggiuntivi. Per un un'esperienza utente migliore, FLAG_CLIENT_RUNNING e È consigliabile utilizzare anche FLAG_CLIENT_IN_FOREGROUND, altrimenti l'utente potrebbe essere sorpresi.

  • Al momento (Android 15) FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED non è implementato. L'app client può semplicemente ignorarlo.

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

Per una migliore esperienza utente, il client mittente PUÒ mostrare un'interfaccia utente se non è presente un flag per iniziare. Ad esempio, se FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED non è impostato, il mittente può mostrare un avviso popup o una finestra di dialogo per chiedere all'utente di sbloccare lo schermo del nella zona degli occupanti del ricevitore.

Quando il mittente non ha più bisogno di trovare i destinatari (ad esempio, quando individua tutti i ricevitori e le connessioni stabilite oppure diventa inattivo), PUÒ interrompere la scoperta.

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

L'interruzione del rilevamento non influisce sulle connessioni esistenti. Il mittente può continua a inviare Payload ai ricevitori connessi.

(Mittente) Richiesta di connessione

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

    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 ricevitore) Accetta la connessione

Quando il mittente richiede una connessione al destinatario, AbstractReceiverService nell'app destinatario sarà associato dal servizio auto, e AbstractReceiverService.onConnectionInitiated() verranno richiamati. Come illustrato in Richiesta di connessione(mittente). onConnectionInitiated() è un metodo astratto e DEVE essere implementato dalla dell'app client.

Quando il destinatario accetta la richiesta di connessione, l'indirizzo Verrà richiamata la funzionalità ConnectionRequestCallback.onConnected(), dopodiché la connessione viene istituito e mantenuto.

(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 nell'elemento Payload. Se il mittente deve inviare altri tipi di dati, DEVE serializzare i dati in un byte usa l'array di byte per creare un oggetto Payload e inviare Payload. Quindi il client ricevente ottiene l'array di byte dall' Payload e deserializza l'array di byte nell'oggetto dati previsto. Ad esempio, se il mittente vuole inviare una stringa hello al destinatario endpoint con ID FragmentB, può utilizzare i buffer Proto per definire un tipo di dati nel seguente modo:

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 ricevitore) Ricevere e inviare il payload

Quando l'app ricevente riceve Payload, la sua AbstractReceiverService.onPayloadReceived() verrà richiamato. Come spiegato nella sezione Invia il payload, onPayloadReceived() è un e DEVE essere implementato dall'app client. In questo metodo, il client PUÒ inoltrare Payload agli endpoint ricevitori corrispondenti oppure memorizzare nella cache Payload quindi inviarlo una volta che l'endpoint di ricezione previsto registrato.

(Endpoint del destinatario) Registrare e annullare la registrazione

L'app del destinatario DEVE chiamare registerReceiver() per registrare il destinatario endpoint. Un caso d'uso tipico è che un frammento deve ricevere Payload, quindi Registra un endpoint ricevente:

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

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

Una volta che AbstractReceiverService nel client destinatario invia il token Payload all'endpoint del destinatario, il PayloadCallback associato sarà richiamato.

L'app client PUÒ registrare più endpoint ricevitore finché Gli asset receiverEndpointId sono univoci tra le app client. receiverEndpointId verrà utilizzato dal AbstractReceiverService per decidere quale ricevente endpoint a cui inviare il payload. Ad esempio:

  • Il mittente specifica receiver_endpoint_id:FragmentB nel campo Payload. Quando riceve Payload, AbstractReceiverService nelle chiamate del destinatario forwardPayload("FragmentB", payload) per inviare il payload a FragmentB
  • Il mittente specifica data_type:VOLUME_CONTROL nel campo Payload. Quando riceve Payload, il AbstractReceiverService del ricevente sa che questo tipo di Payload debba essere inviato a FragmentB, quindi chiama forwardPayload("FragmentB", payload)
di Gemini Advanced.
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Mittente) Termina la connessione

Quando il mittente non deve più 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 il logging:

    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
    

Null CarRemoteDeviceManager e CarOccupantConnectionManager

Controlla queste possibili cause principali:

  1. Si è verificato un incidente del servizio auto. Come illustrato in precedenza, i due manager reimpostata intenzionalmente su null in caso di arresto anomalo del servizio auto. Quando il servizio auto viene riavviato, i due gestori vengono impostati su valori non nulli.

  2. CarRemoteDeviceService o CarOccupantConnectionService non sono in un bucket in cui è abilitato il controllo delle versioni. Per determinare se uno o l'altro è abilitato, esegui:

    adb shell dumpsys car_service --services CarFeatureController
    
    • Cerca mDefaultEnabledFeaturesFromConfig, che dovrebbe 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 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 quando si chiama l'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 uno stack in caso di 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 questo 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.

Client1 può inviare il payload al client2, ma non il contrario

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