Die Multi-Display Communications API kann von einer systemprivilegierten App in AAOS verwendet werden, um mit derselben App (mit demselben Paketnamen) zu kommunizieren, die in einem anderen Insassenbereich in einem Auto ausgeführt wird. Auf dieser Seite wird beschrieben, wie Sie die API einbinden. Weitere Informationen finden Sie unter CarOccupantZoneManager.OccupantZoneInfo.
Aufenthaltsbereich
Das Konzept einer Nutzerzone ordnet einem Nutzer eine Reihe von Displays zu. Jede Zone für Insassen hat ein Display mit dem Typ DISPLAY_TYPE_MAIN. Eine Insassenzone kann auch zusätzliche Displays haben, z. B. ein Kombiinstrument. Jeder Insassenzone wird ein Android-Nutzer zugewiesen. Jeder Nutzer hat eigene Konten und Apps.
Hardwarekonfiguration
Die Comms API unterstützt nur ein einziges SoC. In diesem Modell werden alle Zonen und Nutzer auf demselben SoC ausgeführt. Die Comms API besteht aus drei Komponenten:
Mit der Power Management API kann der Client die Stromversorgung der Displays in den Fahrgastbereichen verwalten.
Mit der Discovery API kann der Client den Status anderer Insassenbereiche im Auto und von ähnlichen Clients in diesen Insassenbereichen überwachen. Verwenden Sie die Discovery API, bevor Sie die Connection API verwenden.
Mit der Connection API kann der Client eine Verbindung zu seinem Peer-Client in einer anderen Zone herstellen und eine Nutzlast an den Peer-Client senden.
Für die Verbindung sind die Discovery API und die Connection API erforderlich. Die Power Management API ist optional.
Die Comms API unterstützt keine Kommunikation zwischen verschiedenen Apps. Stattdessen ist sie nur für die Kommunikation zwischen Apps mit demselben Paketnamen vorgesehen und wird nur für die Kommunikation zwischen verschiedenen sichtbaren Nutzern verwendet.
Integrationsleitfaden
AbstractReceiverService implementieren
Damit die Empfänger-App die Payload empfangen kann, MUSS sie die in AbstractReceiverService definierten abstrakten Methoden implementieren. Beispiel:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
onConnectionInitiated() wird aufgerufen, wenn der Senderclient eine Verbindung zu diesem Empfängerclient anfordert. Wenn zur Herstellung der Verbindung eine Nutzerbestätigung erforderlich ist, MyReceiverService kann diese Methode überschreiben, um eine Berechtigungsaktivität zu starten und acceptConnection() oder rejectConnection() basierend auf dem Ergebnis aufzurufen. Andernfalls MyReceiverService kann einfach acceptConnection() aufrufen.
onPayloadReceived() wird aufgerufen, wenn MyReceiverService ein Payload vom Sender-Client empfangen hat. MyReceiverService kann diese Methode überschreiben, um:
- Leite die
Payloadan die entsprechenden Empfängerendpunkte weiter, sofern vorhanden. Rufen SiegetAllReceiverEndpoints()auf, um die registrierten Empfängerendpunkte abzurufen. Wenn Sie diePayloadan einen bestimmten Empfängerendpunkt weiterleiten möchten, rufen SieforwardPayload()auf.
ODER
Payloadim Cache speichern und senden, wenn der erwartete Empfängerendpunkt registriert ist. DerMyReceiverServicewird überonReceiverRegistered()benachrichtigt.
AbstractReceiverService deklarieren
Die Empfänger-App MUSS die implementierte AbstractReceiverService in ihrer Manifestdatei deklarieren, einen Intent-Filter mit der Aktion android.car.intent.action.RECEIVER_SERVICE für diesen Dienst hinzufügen und die Berechtigung android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE erfordern:
<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>
Die Berechtigung android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE sorgt dafür, dass nur das Framework an diesen Dienst gebunden werden kann. Wenn für diesen Dienst keine Berechtigung erforderlich ist, kann eine andere App möglicherweise eine Verbindung zu diesem Dienst herstellen und direkt eine Payload an ihn senden.
Berechtigung deklarieren
Die Client-App MUSS die Berechtigungen in ihrer Manifestdatei deklarieren.
<!-- 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"/>
Jede der drei oben genannten Berechtigungen ist eine privilegierte Berechtigung, die durch Zulassungslistendateien vorab gewährt werden MUSS. Hier sehen Sie beispielsweise die Zulassungslistendatei der 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>
Autoverwalter erhalten
Damit die API verwendet werden kann, MUSS die Client-App ein CarServiceLifecycleListener registrieren, um die zugehörigen Car-Manager abzurufen:
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);
(Absender) Discover
Bevor eine Verbindung zum Empfängerclient hergestellt wird, SOLLTE der Senderclient den Empfängerclient durch Registrieren eines CarRemoteDeviceManager.StateCallback erkennen:
// 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);
}
Bevor der Absender eine Verbindung zum Empfänger anfordert, SOLLTE er dafür sorgen, dass alle Flags der Empfänger-Bewohnerzone und der Empfänger-App festgelegt sind. Andernfalls können Fehler auftreten. Beispiel:
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;
}
Wir empfehlen, dass der Absender eine Verbindung zum Empfänger nur anfordert, wenn alle Flags des Empfängers gesetzt sind. Es gibt jedoch Ausnahmen:
FLAG_OCCUPANT_ZONE_CONNECTION_READYundFLAG_CLIENT_INSTALLEDsind die Mindestanforderungen für das Herstellen einer Verbindung.Wenn die Empfänger-App eine Benutzeroberfläche anzeigen muss, um die Zustimmung des Nutzers zur Verbindung einzuholen, werden
FLAG_OCCUPANT_ZONE_POWER_ONundFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDzu zusätzlichen Anforderungen. Für eine bessere Nutzererfahrung werden auchFLAG_CLIENT_RUNNINGundFLAG_CLIENT_IN_FOREGROUNDempfohlen, da der Nutzer sonst möglicherweise überrascht ist.Derzeit (Android 15) ist
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDnicht implementiert. Die Client-App kann sie einfach ignorieren.Derzeit (Android 15) unterstützt die Comms API nur mehrere Nutzer auf derselben Android-Instanz, sodass Peer-Apps denselben langen Versionscode (
FLAG_CLIENT_SAME_LONG_VERSION) und dieselbe Signatur (FLAG_CLIENT_SAME_SIGNATURE) haben können. Daher müssen Apps nicht prüfen, ob die beiden Werte übereinstimmen.
Zur Verbesserung der Nutzerfreundlichkeit KANN der Senderclient eine Benutzeroberfläche anzeigen, wenn kein Flag gesetzt ist. Wenn FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED beispielsweise nicht festgelegt ist, kann der Absender einen Toast oder ein Dialogfeld anzeigen, um den Nutzer aufzufordern, den Bildschirm der Empfängerzone zu entsperren.
Wenn der Absender die Empfänger nicht mehr erkennen muss (z. B. wenn er alle Empfänger gefunden und Verbindungen hergestellt hat oder inaktiv wird), KANN er die Erkennung beenden.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Wenn die Erkennung beendet wird, sind vorhandene Verbindungen nicht betroffen. Der Absender kann weiterhin Payload an die verbundenen Empfänger senden.
(Absender) Verbindung anfordern
Wenn alle Flags des Empfängers gesetzt sind, KANN der Absender eine Verbindung zum Empfänger anfordern:
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);
}
(Empfängerdienst) Verbindung akzeptieren
Sobald der Absender eine Verbindung zum Empfänger anfordert, wird die AbstractReceiverService in der Empfänger-App vom Autoservice gebunden und AbstractReceiverService.onConnectionInitiated() wird aufgerufen. Wie im Abschnitt (Sender) Request Connection (Verbindung anfordern (Absender)) beschrieben, ist onConnectionInitiated() eine abstrahierte Methode, die von der Client-App implementiert werden MUSS.
Wenn der Empfänger die Verbindungsanfrage akzeptiert, wird die ConnectionRequestCallback.onConnected() des Absenders aufgerufen und die Verbindung wird hergestellt.
(Absender) Nutzlast senden
Sobald die Verbindung hergestellt ist, kann der Absender Payload an den Empfänger senden:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
Der Absender kann ein Binder-Objekt oder ein Byte-Array in die Payload einfügen. Wenn der Absender andere Datentypen senden muss, MUSS er die Daten in ein Byte-Array serialisieren, das Byte-Array zum Erstellen eines Payload-Objekts verwenden und das Payload-Objekt senden. Anschließend ruft der Empfängerclient das Byte-Array aus dem empfangenen Payload ab und deserialisiert es in das erwartete Datenobjekt.
Wenn der Absender beispielsweise den String hello an den Empfängerendpunkt mit der ID FragmentB senden möchte, kann er mit Proto Buffers einen Datentyp wie diesen definieren:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
Abbildung 1 zeigt den Payload-Ablauf:
(Receiver-Dienst) Nutzlast empfangen und senden
Sobald die Receiver-App die Payload empfängt, wird ihr AbstractReceiverService.onPayloadReceived() aufgerufen. Wie unter Nutzlast senden beschrieben, ist onPayloadReceived() eine abstrakte Methode, die von der Client-App implementiert werden MUSS. In dieser Methode kann der Client die Payload an die entsprechenden Empfängerendpunkte weiterleiten oder die Payload im Cache speichern und dann senden, sobald der erwartete Empfängerendpunkt registriert ist.
(Empfängerendpunkt) Registrieren und Registrierung aufheben
Die Empfänger-App SOLLTE registerReceiver() aufrufen, um die Empfängerendpunkte zu registrieren. Ein typischer Anwendungsfall ist, dass ein Fragment den Empfänger Payload benötigt und daher einen Empfängerendpunkt registriert:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Sobald das AbstractReceiverService im Empfängerclient das Payload an den Empfängerendpunkt sendet, wird der zugehörige PayloadCallback aufgerufen.
Die Client-App KANN mehrere Empfängerendpunkte registrieren, sofern ihre receiverEndpointIds innerhalb der Client-App eindeutig sind. Die receiverEndpointId wird von der AbstractReceiverService verwendet, um zu entscheiden, an welchen Empfängerendpunkt bzw. welche Empfängerendpunkte die Nutzlast gesendet werden soll. Beispiel:
- Der Absender gibt
receiver_endpoint_id:FragmentBin derPayloadan. Beim Empfang vonPayloadruftAbstractReceiverServiceim EmpfängerforwardPayload("FragmentB", payload)auf, um die Nutzlast anFragmentBzu senden. - Der Absender gibt
data_type:VOLUME_CONTROLin derPayloadan. Wenn der Empfänger diePayloadempfängt, weiß dieAbstractReceiverService, dass diese Art vonPayloadanFragmentBgesendet werden soll. Daher wirdforwardPayload("FragmentB", payload)aufgerufen.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Absender) Verbindung beenden
Wenn der Absender nicht mehr Payload an den Empfänger senden muss (z. B. wenn er inaktiv wird), SOLLTE er die Verbindung beenden.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Nach dem Trennen der Verbindung kann der Absender keine Payload mehr an den Empfänger senden.
Verbindungsablauf
Ein Verbindungsablauf ist in Abbildung 2 dargestellt.
Fehlerbehebung
Logs prüfen
So prüfen Sie die entsprechenden Logs:
Führen Sie diesen Befehl für die Protokollierung aus:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"So geben Sie den internen Status von
CarRemoteDeviceServiceundCarOccupantConnectionServiceaus:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager und CarOccupantConnectionManager
Hier sind einige mögliche Ursachen:
Der Autoservice ist abgestürzt. Wie bereits gezeigt, werden die beiden Manager absichtlich auf
nullzurückgesetzt, wenn der Autoservice abstürzt. Wenn der Autoservice neu gestartet wird, werden die beiden Manager auf Werte ungleich null gesetzt.Entweder
CarRemoteDeviceServiceoderCarOccupantConnectionServiceist nicht aktiviert. Führen Sie Folgendes aus, um festzustellen, ob eine der beiden Optionen aktiviert ist:adb shell dumpsys car_service --services CarFeatureControllerSuchen Sie nach
mDefaultEnabledFeaturesFromConfig, dascar_remote_device_serviceundcar_occupant_connection_serviceenthalten sollte. Beispiel: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]Standardmäßig sind diese beiden Dienste deaktiviert. Wenn ein Gerät mehrere Displays unterstützt, MÜSSEN Sie diese Konfigurationsdatei überlagern. Sie können die beiden Dienste in einer Konfigurationsdatei aktivieren:
// 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>
Ausnahme beim Aufrufen der API
Wenn die Client-App die API nicht wie vorgesehen verwendet, kann eine Ausnahme auftreten. In diesem Fall kann die Client-App die Nachricht in der Ausnahme und den Absturz-Stack prüfen, um das Problem zu beheben. Beispiele für den Missbrauch der API:
registerStateCallback()Dieser Kunde hat bereits eineStateCallbackregistriert.unregisterStateCallback()Von dieserCarRemoteDeviceManager-Instanz wurde keineStateCallbackregistriert.registerReceiver()receiverEndpointIdist bereits registriert.unregisterReceiver()receiverEndpointIdist nicht registriert.requestConnection()Es besteht bereits eine ausstehende oder aktive Verbindung.cancelConnection()Es gibt keine ausstehende Verbindung, die abgebrochen werden kann.sendPayload()Keine Verbindung hergestellt.disconnect()Keine Verbindung hergestellt.
Client1 kann Nutzlast an Client2 senden, aber nicht umgekehrt
Die Verbindung ist standardmäßig unidirektional. Für eine bidirektionale Verbindung müssen sowohl client1 als auch client2 eine Verbindung zueinander anfordern und dann die Genehmigung erhalten.