Multi-Display Communications API

Die Multi-Display Communications API kann von einer App mit Systemberechtigungen in AAOS verwendet werden, um mit derselben App (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.

Zone für Personen

Mit dem Konzept einer Belegungszone wird ein Nutzer einer Reihe von Displays zugeordnet. Jede Belegungszone hat ein Display vom Typ DISPLAY_TYPE_MAIN. Eine Belegungszone kann auch zusätzliche Displays haben, z. B. ein Cluster-Display. Jedem Insassenbereich wird ein Android-Nutzer zugewiesen. Jeder Nutzer hat eigene Konten und Apps.

Hardwarekonfiguration

Die Comms API unterstützt nur ein einzelnes SoC. Bei diesem Modell werden alle Zonen für Mitfahrer und Nutzer auf demselben SoC ausgeführt. Die Comms API besteht aus drei Komponenten:

  • Mit der Energieverwaltungs-API kann der Kunde die Stromversorgung der Displays in den Belegungszonen verwalten.

  • Mit der Discovery API kann der Client den Status anderer Insassenbereiche im Auto und anderer 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 Belegungszone 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 er 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 Payload empfangen werden kann, MUSS die Empfänger-App 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 Absenderclient eine Verbindung zu diesem Empfängerclient anfordert. Wenn für die Herstellung der Verbindung eine Nutzerbestätigung erforderlich ist, kann MyReceiverService diese Methode überschreiben, um eine Berechtigungsaktivität zu starten und je nach Ergebnis acceptConnection() oder rejectConnection() aufzurufen. Andernfalls kann MyReceiverService acceptConnection() aufrufen.`

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` kann diese Methode überschreiben, um:

  • Leiten Sie die Payload an die entsprechenden Empfängerendpunkte weiter, falls vorhanden. Rufen Sie getAllReceiverEndpoints() auf, um die registrierten Empfängerendpunkte abzurufen. Wenn du die Payload an einen bestimmten Empfängerendpunkt weiterleiten möchtest, ruf forwardPayload() auf.

ODER

  • Speichere die Payload im Cache und sende sie, wenn der erwartete Empfängerendpunkt registriert ist, über den die MyReceiverService über onReceiverRegistered() benachrichtigt wird.

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 anfordern:

<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 android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE-Berechtigung sorgt dafür, dass nur das Framework eine Bindung an diesen Dienst herstellen kann. Wenn für diesen Dienst keine Berechtigung erforderlich ist, kann eine andere App möglicherweise an diesen Dienst binden und ihm direkt eine Payload 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"/>

Alle drei oben genannten Berechtigungen sind Berechtigungen mit erhöhten Befugnissen, die VORHER durch Zulassungslistendateien gewährt werden MÜSSEN. Hier ist beispielsweise die Zulassungsliste der MultiDisplayTest-App:

// 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>

Fahrzeugmanager

Wenn Sie die API verwenden möchten, MUSS die Client-App einen CarServiceLifecycleListener registrieren, um die zugehörigen Fahrzeugmanager 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 Absenderclient den Empfängerclient finden, indem er eine CarRemoteDeviceManager.StateCallback registriert:

// 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 Besetztzone und der App des Empfängers gesetzt 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 dem Absender, eine Verbindung zum Empfänger nur anzufordern, 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 die Herstellung einer Verbindung.

  • Wenn die Empfänger-App eine Benutzeroberfläche anzeigen muss, um die Nutzergenehmigung für die Verbindung einzuholen, sind FLAG_OCCUPANT_ZONE_POWER_ON und FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED zusätzliche Anforderungen. Für eine bessere Nutzerfreundlichkeit empfehlen wir auch FLAG_CLIENT_RUNNING und FLAG_CLIENT_IN_FOREGROUND, da Nutzer sonst möglicherweise überrascht sein könnten.

  • 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 in 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.

Für eine bessere Nutzerfreundlichkeit kann der Absenderclient eine Benutzeroberfläche anzeigen, wenn kein Flag gesetzt ist. Wenn FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED beispielsweise nicht festgelegt ist, kann der Absender ein Toast- oder Dialogfeld anzeigen, um den Nutzer zum Entsperren des Bildschirms der Zone des Nutzers aufzufordern.

Wenn der Absender die Empfänger nicht mehr finden muss (z. B. wenn er alle Empfänger gefunden und Verbindungen hergestellt hat oder inaktiv wird), kann er die Suche beenden.

if (mRemoteDeviceManager != null) {
    mRemoteDeviceManager.unregisterStateCallback();
}

Wenn die Suche 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 AbstractReceiverService in der Empfänger-App vom Autodienst gebunden und AbstractReceiverService.onConnectionInitiated() wird aufgerufen. Wie unter (Sender) Request Connection erläutert, ist onConnectionInitiated() eine abstrakte Methode und MUSS von der Client-App implementiert werden.

Wenn der Empfänger die Verbindungsanfrage akzeptiert, wird die ConnectionRequestCallback.onConnected() des Absenders aufgerufen und die Verbindung hergestellt.

(Absender) Nutzlast senden

Nachdem die Verbindung hergestellt wurde, 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 das Payload einfügen. Wenn der Absender andere Datentypen senden muss, MUSS er die Daten in ein Bytearray serialisieren, mit dem Bytearray ein Payload-Objekt erstellen und das Payload senden. Anschließend ruft der Empfängerclient das Byte-Array aus der empfangenen Payload ab und deserialisiert es in das erwartete Datenobjekt. Wenn der Absender beispielsweise einen String hello an den Empfängerendpunkt mit der ID FragmentB senden möchte, kann er mit Proto Buffers einen Datentyp so definieren:

message MyData {
  required string receiver_endpoint_id = 1;
  required string data = 2;
}

Abbildung 1 zeigt den Ablauf von Payload:

Nutzlast senden

Abbildung 1. Senden Sie die Nutzlast.

(Empfängerdienst) Nutzlast empfangen und versenden

Sobald die Empfänger-App die Payload empfängt, wird ihre AbstractReceiverService.onPayloadReceived() aufgerufen. Wie unter Nutzlast senden erläutert, ist onPayloadReceived() eine abstrakte Methode und MUSS von der Client-App implementiert werden. Bei 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 Registrieren aufheben

Die Empfänger-App MUSS registerReceiver() aufrufen, um die Empfängerendpunkte zu registrieren. Ein typischer Anwendungsfall ist, dass ein Fragment Payload empfangen muss. Dazu wird ein Empfängerendpunkt registriert:

private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
    …
};

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.registerReceiver("FragmentB",
                getActivity().getMainExecutor(), mPayloadCallback);
}

Sobald die AbstractReceiverService im Empfängerclient die Payload an den Empfängerendpunkt sendet, wird die zugehörige PayloadCallback aufgerufen.

Die Client-App KANN mehrere Empfängerendpunkte registrieren, solange ihre receiverEndpointId in der Client-App eindeutig sind. Die receiverEndpointId wird vom AbstractReceiverService verwendet, um zu entscheiden, an welche Empfängerendpunkte die Nutzlast gesendet werden soll. Beispiel:

  • Der Absender gibt receiver_endpoint_id:FragmentB in Payload an. Beim Empfang der Payload ruft der AbstractReceiverService im Empfänger forwardPayload("FragmentB", payload) auf, um die Nutzlast an FragmentB zu senden.
  • Der Absender gibt data_type:VOLUME_CONTROL in Payload an. Beim Empfang des Payload weiß der AbstractReceiverService im Empfänger, dass diese Art von Payload an FragmentB gesendet werden soll. Er ruft daher forwardPayload("FragmentB", payload) auf.
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Sender) Verbindung beenden

Sobald der Absender Payload nicht mehr an den Empfänger senden muss (z. B. wenn er inaktiv wird), sollte er die Verbindung beenden.

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.disconnect(receiverZone);
}

Nach der Trennung kann der Absender keine Payload mehr an den Empfänger senden.

Verbindungsablauf

Ein Verbindungsablauf ist in Abbildung 2 dargestellt.

Verbindungsablauf

Abbildung 2. Verbindungsfluss.

Fehlerbehebung

Logs prüfen

So prüfen Sie die entsprechenden Protokolle:

  1. Führen Sie diesen Befehl aus, um Protokolle zu erstellen:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
    
  2. So rufen Sie den internen Status von CarRemoteDeviceService und CarOccupantConnectionService ab:

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
    

Null CarRemoteDeviceManager und CarOccupantConnectionManager

Mögliche Ursachen:

  1. Der Autodienst ist abgestürzt. Wie bereits erwähnt, werden die beiden Manager bei einem Absturz des Autodienstes absichtlich auf null zurückgesetzt. Wenn der Autodienst neu gestartet wird, werden die beiden Manager auf Nicht-Null-Werte gesetzt.

  2. CarRemoteDeviceService oder CarOccupantConnectionService ist nicht aktiviert. Führen Sie den folgenden Befehl 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]
      
    • Diese beiden Dienste sind standardmäßig 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 Crash-Stack prüfen, um das Problem zu beheben. Beispiele für den Missbrauch der API:

  • registerStateCallback() Dieser Kunde hat bereits eine StateCallback registriert.
  • unregisterStateCallback() Es wurde keine StateCallback über diese CarRemoteDeviceManager-Instanz registriert.
  • registerReceiver() receiverEndpointId ist bereits registriert.
  • unregisterReceiver() receiverEndpointId ist nicht registriert.
  • requestConnection() Es gibt bereits eine ausstehende oder bestehende Verbindung.
  • cancelConnection() Es gibt keine ausstehende Verbindung, die storniert werden könnte.
  • sendPayload() Keine Verbindung hergestellt.
  • disconnect() Keine Verbindung hergestellt.

Client 1 kann Nutzlast an Client 2 senden, aber nicht umgekehrt

Die Verbindung ist standardmäßig einseitig. Um eine bidirektionale Verbindung herzustellen, MÜSSEN sowohl client1 als auch client2 eine Verbindung zueinander anfordern und dann die Genehmigung erhalten.