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
Payload
an die entsprechenden Empfängerendpunkte weiter, sofern vorhanden. Rufen SiegetAllReceiverEndpoints()
auf, um die registrierten Empfängerendpunkte abzurufen. Wenn Sie diePayload
an einen bestimmten Empfängerendpunkt weiterleiten möchten, rufen SieforwardPayload()
auf.
ODER
Payload
im Cache speichern und senden, wenn der erwartete Empfängerendpunkt registriert ist. DerMyReceiverService
wird ü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_READY
undFLAG_CLIENT_INSTALLED
sind 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_ON
undFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
zu zusätzlichen Anforderungen. Für eine bessere Nutzererfahrung werden auchFLAG_CLIENT_RUNNING
undFLAG_CLIENT_IN_FOREGROUND
empfohlen, da der Nutzer sonst möglicherweise überrascht ist.Derzeit (Android 15) ist
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
nicht 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 receiverEndpointId
s 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:FragmentB
in derPayload
an. Beim Empfang vonPayload
ruftAbstractReceiverService
im EmpfängerforwardPayload("FragmentB", payload)
auf, um die Nutzlast anFragmentB
zu senden. - Der Absender gibt
data_type:VOLUME_CONTROL
in derPayload
an. Wenn der Empfänger diePayload
empfängt, weiß dieAbstractReceiverService
, dass diese Art vonPayload
anFragmentB
gesendet 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
CarRemoteDeviceService
undCarOccupantConnectionService
aus: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
null
zurückgesetzt, wenn der Autoservice abstürzt. Wenn der Autoservice neu gestartet wird, werden die beiden Manager auf Werte ungleich null gesetzt.Entweder
CarRemoteDeviceService
oderCarOccupantConnectionService
ist nicht aktiviert. Führen Sie Folgendes aus, um festzustellen, ob eine der beiden Optionen aktiviert ist:adb shell dumpsys car_service --services CarFeatureController
Suchen Sie nach
mDefaultEnabledFeaturesFromConfig
, dascar_remote_device_service
undcar_occupant_connection_service
enthalten 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 eineStateCallback
registriert.unregisterStateCallback()
Von dieserCarRemoteDeviceManager
-Instanz wurde keineStateCallback
registriert.registerReceiver()
receiverEndpointId
ist bereits registriert.unregisterReceiver()
receiverEndpointId
ist 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.