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 una zona degli occupanti diversa in un'auto. Questa pagina descrive come integrare l'API. Per saperne di più, puoi anche consultare CarOccupantZoneManager.OccupantZoneInfo.
Zona degli occupanti
Il concetto di zona degli occupanti associa un utente a un insieme di display. Ogni zona degli occupanti ha un display di tipo DISPLAY_TYPE_MAIN. Una zona degli occupanti può avere anche 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 proprie app.
Configurazione hardware
L'API Comms supporta un solo SoC. Nel modello a SoC singolo, tutte le zone degli occupanti e tutti gli utenti 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 degli occupanti.
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 degli occupanti. 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 degli occupanti e di inviare un payload al client peer.
L'API Discovery e l'API Connection sono necessarie per la connessione. L'API di gestione dell'alimentazione è facoltativa.
L'API Comms non supporta la comunicazione tra app diverse. È invece progettata solo per la comunicazione tra app con lo stesso nome di pacchetto e utilizzata solo per la comunicazione tra utenti visibili diversi.
Guida all'integrazione
Implementare AbstractReceiverService
Per ricevere il Payload, l'app del destinatario 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 richiamato quando il client del mittente richiede una connessione a questo client del destinatario. Se è necessaria la conferma dell'utente per stabilire la connessione, MyReceiverService può sostituire questo metodo per avviare un'attività di autorizzazione e chiamare acceptConnection() o rejectConnection() in base al risultato. In caso contrario, MyReceiverService può semplicemente chiamare acceptConnection().
onPayloadReceived() viene richiamato quando MyReceiverService ha ricevuto un
Payload dal client del mittente. MyReceiverService può sostituire questo metodo per:
- Inoltrare il
Payloadagli endpoint del destinatario corrispondenti, se presenti. Per ottenere gli endpoint del destinatario registrati, chiamagetAllReceiverEndpoints(). Per inoltrare ilPayloada un determinato endpoint del destinatario, chiamaforwardPayload()
OPPURE
- Memorizzare nella cache il
Payloade inviarlo quando viene registrato l'endpoint del destinatario previsto, per il qualeMyReceiverServicericeve una notifica tramiteonReceiverRegistered()
Dichiarare AbstractReceiverService
L'app del destinatario DEVE dichiarare l'oggetto AbstractReceiverService implementato nel 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'altra app 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-concessa 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>
Ottenere i gestori di Car
Per utilizzare l'API, l'app client DEVE registrare un CarServiceLifecycleListener per ottenere i gestori di Car 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) Scoprire
Prima di connettersi al client del destinatario, il client del mittente DOVREBBE scoprire il client del destinatario 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 DOVREBBE assicurarsi che tutti i flag della zona degli occupanti del destinatario 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;
}
Ti consigliamo di richiedere una connessione al destinatario solo quando tutti i flag del destinatario sono impostati. Tuttavia, esistono delle eccezioni:
FLAG_OCCUPANT_ZONE_CONNECTION_READYeFLAG_CLIENT_INSTALLEDsono i requisiti minimi necessari per stabilire una connessione.Se l'app del destinatario deve mostrare un'interfaccia utente per ottenere l'approvazione della connessione da parte dell'utente,
FLAG_OCCUPANT_ZONE_POWER_ONeFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDdiventano requisiti aggiuntivi. Per una migliore esperienza utente, sono consigliati ancheFLAG_CLIENT_RUNNINGeFLAG_CLIENT_IN_FOREGROUND, altrimenti l'utente potrebbe rimanere sorpreso.Per il momento (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDnon è implementato. L'app client può semplicemente ignorarlo.Per il momento (Android 15), l'API Comms supporta solo più utenti sulla stessa istanza di 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 corrispondano.
Per una migliore esperienza utente, il client del mittente PUÒ mostrare un'interfaccia utente se un flag non è impostato. Ad esempio, se FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED non è impostato, il mittente può mostrare una notifica toast o una finestra di dialogo per invitare l'utente a sbloccare lo schermo della zona degli occupanti del destinatario.
Quando il mittente non ha più bisogno di scoprire i destinatari (ad esempio, quando trova tutti i destinatari e stabilisce le connessioni o diventa inattivo), PUÒ interrompere la scoperta.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Quando la scoperta viene interrotta, le connessioni esistenti non vengono interessate. Il mittente può continuare a inviare Payload ai destinatari connessi.
(Mittente) Richiedere la 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 del destinatario) Accettare la connessione
Una volta che il mittente richiede una connessione al destinatario, il
AbstractReceiverService nell'app del destinatario verrà associato dal servizio auto,
e AbstractReceiverService.onConnectionInitiated() verrà richiamato. Come
spiegato in (Mittente) Richiedere la connessione,
onConnectionInitiated() è un metodo astratto e DEVE essere implementato dall'
app client.
Quando il destinatario accetta la richiesta di connessione, viene richiamato ConnectionRequestCallback.onConnected() del mittente e la connessione viene stabilita.
(Mittente) Inviare 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 nel 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 costruire un oggetto Payload e inviare il Payload. Il client del destinatario riceve quindi l'array di byte dal Payload ricevuto 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 dati come questo:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
La Figura 1 illustra il flusso di Payload:
(Servizio del destinatario) Ricevere e inviare il payload
Una volta che l'app del destinatario riceve il Payload, viene richiamato il suo
AbstractReceiverService.onPayloadReceived(). Come spiegato in
Inviare il payload, onPayloadReceived() è un
metodo astratto e DEVE essere implementato dall'app client. In questo metodo, il
client PUÒ inoltrare il Payload agli endpoint del destinatario corrispondenti o
memorizzare nella cache il Payload e inviarlo una volta registrato l'endpoint del destinatario previsto.
(Endpoint del destinatario) Registrare e annullare la registrazione
L'app del destinatario DOVREBBE chiamare registerReceiver() per registrare gli endpoint del destinatario. Un caso d'uso tipico è che un Fragment deve ricevere Payload, quindi registra un endpoint del destinatario:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Una volta che AbstractReceiverService nel client del destinatario invia
Payload all'endpoint del destinatario, viene
richiamato il PayloadCallback associato.
L'app client PUÒ registrare più endpoint del destinatario purché i relativi receiverEndpointId siano univoci nell'app client. receiverEndpointId verrà utilizzato da AbstractReceiverService per decidere a quali endpoint del destinatario inviare il payload. Ad esempio:
- Il mittente specifica
receiver_endpoint_id:FragmentBnelPayload. Quando riceve ilPayload, ilAbstractReceiverServicenel destinatario chiamaforwardPayload("FragmentB", payload)per inviare il payload aFragmentB - Il mittente specifica
data_type:VOLUME_CONTROLnelPayload. Quando riceve ilPayload, ilAbstractReceiverServicenel destinatario sa che questo tipo diPayloaddeve essere inviato aFragmentB, quindi chiamaforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Mittente) Terminare la connessione
Una volta che 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.
Risoluzione dei problemi
Controllare i log
Per controllare i log corrispondenti:
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"Per eseguire il dump dello stato interno di
CarRemoteDeviceServiceeCarOccupantConnectionService:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
CarRemoteDeviceManager e CarOccupantConnectionManager null
Esamina queste possibili cause principali:
Il servizio auto è andato in arresto anomalo. Come illustrato in precedenza, i due gestori vengono reimpostati intenzionalmente su
nullquando il servizio auto va in arresto anomalo. Quando il servizio auto viene riavviato, i due gestori vengono impostati su valori non null.CarRemoteDeviceServiceoCarOccupantConnectionServicenon è abilitato. Per determinare se uno o l'altro è abilitato, esegui:adb shell dumpsys car_service --services CarFeatureControllerCerca
mDefaultEnabledFeaturesFromConfig, che deve contenerecar_remote_device_serviceecar_occupant_connection_service. Ad 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 disabilitati. Quando un dispositivo supporta più display, DEVI sovrapporre questo file di configurazione. Puoi abilitare 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 di arresto anomalo per risolvere il problema. Ecco alcuni esempi di uso improprio dell'API:
registerStateCallback()Questo client ha già registrato unStateCallback.unregisterStateCallback()NessunStateCallbackè stato registrato da questaCarRemoteDeviceManageristanza.registerReceiver()receiverEndpointIdè già registrato.unregisterReceiver()receiverEndpointIdnon è 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 a client2, ma non viceversa
La connessione è unidirezionale per progettazione. Per stabilire una connessione bidirezionale, sia client1 sia client2 DEVONO richiedere una connessione reciproca e poi ottenere l'approvazione.