Multi-Display Communications API

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 Sie getAllReceiverEndpoints() auf, um die registrierten Empfängerendpunkte abzurufen. Wenn Sie die Payload an einen bestimmten Empfängerendpunkt weiterleiten möchten, rufen Sie forwardPayload() auf.

ODER

  • Payload im Cache speichern und senden, wenn der erwartete Empfängerendpunkt registriert ist. Der MyReceiverService wird über onReceiverRegistered() 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 und FLAG_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 und FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED zu zusätzlichen Anforderungen. Für eine bessere Nutzererfahrung werden auch FLAG_CLIENT_RUNNING und FLAG_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:

Nutzlast senden

Abbildung 1. Senden Sie die Nutzlast.

(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:FragmentB in der Payload an. Beim Empfang von Payload ruft AbstractReceiverService im Empfänger forwardPayload("FragmentB", payload) auf, um die Nutzlast an FragmentB zu senden.
  • Der Absender gibt data_type:VOLUME_CONTROL in der Payload an. Wenn der Empfänger die Payload empfängt, weiß die AbstractReceiverService, dass diese Art von Payload an FragmentB gesendet werden soll. Daher wird forwardPayload("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.

Verbindungsablauf

Abbildung 2. Verbindungsablauf.

Fehlerbehebung

Logs prüfen

So prüfen Sie die entsprechenden Logs:

  1. 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"
  2. So geben Sie den internen Status von CarRemoteDeviceService und CarOccupantConnectionService 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:

  1. 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.

  2. Entweder CarRemoteDeviceService oder CarOccupantConnectionService 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, das car_remote_device_service und car_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 eine StateCallback registriert.
  • unregisterStateCallback() Von dieser CarRemoteDeviceManager-Instanz wurde keine StateCallback 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.