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 saperne di più, puoi anche consultare CarOccupantZoneManager.OccupantZoneInfo.
Zona occupante
Il concetto di zona occupante associa un utente a un insieme di display. Ogni zona occupanti ha un display di tipo DISPLAY_TYPE_MAIN. Una zona occupante può anche avere display aggiuntivi, come un display del quadro strumenti. 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 a singolo SoC, tutte le zone e gli utenti dell'abitacolo 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 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 quelle 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 occupante 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 Payload
, l'app ricevente 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 mittente richiede una
connessione a questo client destinatario. Se è necessaria la conferma dell'utente per stabilire
la connessione, MyReceiverService
può ignorare 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 mittente. MyReceiverService
può sostituire questo
metodo per:
- Inoltra
Payload
agli endpoint del destinatario corrispondenti, se presenti. Per ottenere gli endpoint del ricevitore registrato, chiamagetAllReceiverEndpoints()
. Per inoltrarePayload
a un determinato endpoint ricevitore, chiamaforwardPayload()
OPPURE
- Memorizza nella cache
Payload
e invialo quando viene registrato l'endpoint del destinatario previsto, per il qualeMyReceiverService
viene avvisato tramiteonReceiverRegistered()
Dichiara AbstractReceiverService
L'app ricevitore DEVE dichiarare l'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 associarsi a questo
servizio e inviargli un Payload
direttamente.
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"/>
Ciascuna delle tre autorizzazioni riportate sopra è un'autorizzazione privilegiata, 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 auto
Per utilizzare l'API, l'app client DEVE registrare un CarServiceLifecycleListener
per
ottenere i gestori 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 destinatario, il client mittente DEVE rilevare il
client 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 DEVE assicurarsi che tutti i flag della zona di occupazione 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;
}
Consigliamo al mittente di richiedere una connessione al destinatario solo quando tutti i flag del destinatario sono impostati. Detto questo, esistono delle eccezioni:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
eFLAG_CLIENT_INSTALLED
sono i requisiti minimi necessari per stabilire una connessione.Se l'app ricevitore deve mostrare una UI per ottenere l'approvazione della connessione da parte dell'utente,
FLAG_OCCUPANT_ZONE_POWER_ON
eFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
diventano requisiti aggiuntivi. Per una migliore esperienza utente, sono consigliati ancheFLAG_CLIENT_RUNNING
eFLAG_CLIENT_IN_FOREGROUND
, altrimenti l'utente potrebbe rimanere sorpreso.Per il momento (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
non è implementato. L'app client può semplicemente ignorarlo.Per ora (Android 15), l'API Comms supporta 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 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 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 avviso o una finestra di dialogo per chiedere all'utente di sbloccare lo schermo della
zona occupante del ricevitore.
Quando il mittente non ha più bisogno di rilevare i destinatari (ad esempio, quando trova tutti i destinatari e stabilisce le connessioni o diventa inattivo), PUÒ interrompere il rilevamento.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Quando la rilevazione viene interrotta, le connessioni esistenti non vengono interessate. Il mittente può
continuare a inviare Payload
ai ricevitori connessi.
(Mittente) Richiedi 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 destinatario) Accetta la connessione
Una volta che il mittente richiede una connessione al destinatario, il
AbstractReceiverService
nell'app del destinatario sarà vincolato al servizio auto
e verrà richiamato AbstractReceiverService.onConnectionInitiated()
. Come
spiegato in (Mittente) Richiedi 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, quindi 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 costruire un oggetto Payload
e inviare
Payload
. Il client destinatario riceve l'array di byte da Payload
ricevuto e lo deserializza nell'oggetto dati previsto.
Ad esempio, se il mittente vuole inviare una stringa hello
all'endpoint 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 Payload
:
(Servizio ricevitore) Ricevi e invia il payload
Una volta che l'app ricevitore riceve l'Payload
, viene richiamato il relativo
AbstractReceiverService.onPayloadReceived()
. Come spiegato in
Invia il payload, onPayloadReceived()
è un
metodo astratto e DEVE essere implementato dall'app client. In questo metodo, il
client PUÒ inoltrare Payload
agli endpoint del destinatario corrispondenti o
memorizzare nella cache Payload
e inviarlo una volta registrato l'endpoint del destinatario previsto.
(Endpoint ricevitore) Registrazione e annullamento della registrazione
L'app ricevitore DEVE chiamare registerReceiver()
per registrare gli endpoint del ricevitore. Un caso d'uso tipico è che un frammento deve ricevere Payload
, quindi
registra un endpoint ricevitore:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Una volta che AbstractReceiverService
nel client ricevitore invia
Payload
all'endpoint ricevitore, viene richiamato
PayloadCallback
associato.
L'app client PUÒ registrare più endpoint ricevitore a condizione che i relativi
receiverEndpointId
siano univoci nell'app client. receiverEndpointId
verrà utilizzato da AbstractReceiverService
per decidere a quali endpoint ricevitore
inviare il payload. Ad esempio:
- Il mittente specifica
receiver_endpoint_id:FragmentB
inPayload
. Quando ricevePayload
,AbstractReceiverService
nel ricevitore chiamaforwardPayload("FragmentB", payload)
per inviare il payload aFragmentB
- Il mittente specifica
data_type:VOLUME_CONTROL
inPayload
. Quando ricevePayload
,AbstractReceiverService
nel ricevitore sa che questo tipo diPayload
deve essere inviato aFragmentB
, quindi chiamaforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Mittente) Termina 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 scaricare lo stato interno di
CarRemoteDeviceService
eCarOccupantConnectionService
:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager and CarOccupantConnectionManager
Controlla queste possibili cause principali:
Il servizio di auto ha subito un arresto anomalo. Come illustrato in precedenza, i due gestori vengono reimpostati 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.CarRemoteDeviceService
oCarOccupantConnectionService
non è attivato. Per determinare se una delle due è abilitata, esegui:adb shell dumpsys car_service --services CarFeatureController
Cerca
mDefaultEnabledFeaturesFromConfig
, che dovrebbe contenerecar_remote_device_service
ecar_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 più 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 nello stack di arresto anomalo per risolvere il problema. Esempi di utilizzo improprio dell'API:
registerStateCallback()
Questo cliente ha già registrato unStateCallback
.unregisterStateCallback()
NessunStateCallback
è stato registrato da questa istanza diCarRemoteDeviceManager
.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 Payload a client2, ma non viceversa
La connessione è unidirezionale per impostazione predefinita. Per stabilire una connessione bidirezionale, sia
client1
che client2
DEVONO richiedere una connessione reciproca e poi
ottenere l'approvazione.