Multi-Display Communications API

Die Multi-Display Communications API kann von einer privilegierten System-App in AAOS, um mit derselben App (gleicher Paketname) zu kommunizieren, die in einem anderen Insassenbereich in einem Auto. Auf dieser Seite wird beschrieben, wie die API integriert wird. Weitere Informationen können Sie auch sehen, CarOccupantZoneManager.OccupantZoneInfo.

Insassenbereich

Mit dem Konzept der Insassenzone wird ein Nutzer einer Gruppe von Displays zugeordnet. Jedes Insassenbereich ein Display mit dem Typ DISPLAY_TYPE_MAIN ist. Eine Insassenzone kann auch zusätzliche Bildschirme haben, z. B. ein Cluster-Display. Jedem Insassenbereich ist ein Android-Nutzer zugewiesen. Jeder Nutzer hat ein eigenes Konto und Apps.

Hardwarekonfiguration

Die Comms API unterstützt nur ein einziges SoC. Bei einem einzelnen SoC-Modell sind alle Insassen Zonen und Nutzer werden auf demselben SoC ausgeführt. Die Comms API besteht aus drei Komponenten:

  • Mit der Power Management API kann der Client die Leistung des in den Insassenbereichen angezeigt.

  • Mit der Discovery API kann der Client den Status anderer Insassen überwachen. und Peer-Clients in diesen Insassenbereichen überwachen. Verwenden Sie bevor Sie die Connection API verwenden.

  • Mit der Connection API kann der Client eine Verbindung zu seinem Peer-Client herstellen, und eine Nutzlast an den Peer-Client zu senden.

Für die Verbindung sind die Discovery API und die Connection API erforderlich. Die Macht Management API ist optional.

Die Comms API unterstützt keine Kommunikation zwischen verschiedenen Apps. Stattdessen er ist 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

Zum Empfangen von Payload MÜSSEN die abstrakten Methoden in der Empfänger-App implementiert werden definiert in AbstractReceiverService. 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 ein Verbindung zu diesem Empfängerclient herstellen. Ist eine Bestätigung des Nutzers erforderlich, der Verbindung, kann MyReceiverService diese Methode überschreiben, um ein Berechtigungsaktivität und acceptConnection() oder rejectConnection() aufrufen für das Ergebnis. Andernfalls kann MyReceiverService einfach anrufen acceptConnection().

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverServicekann diese Änderung überschreiben zu:

  • Leite die Payload an die entsprechenden Empfängerendpunkte weiter, falls vorhanden. Bis Rufen Sie getAllReceiverEndpoints() auf, um die registrierten Empfängerendpunkte abzurufen. Bis Payload an einen bestimmten Empfängerendpunkt weiterleiten, forwardPayload() aufrufen

ODER

  • Speichern Sie die Payload im Cache und senden Sie sie, wenn der erwartete Empfängerendpunkt registriert, über die die MyReceiverService benachrichtigt wird durch onReceiverRegistered()

AbstractReceiverService deklarieren

Die Empfänger-App MÜSSEN die implementierte AbstractReceiverService in ihrer Manifestdatei, Intent-Filter mit Aktion hinzufügen android.car.intent.action.RECEIVER_SERVICE für diesen Dienst und erfordern die android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE-Berechtigung:

<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 stellt sicher, dass nur das Framework an diesen Dienst gebunden werden kann. Wenn dieser Dienst keine Berechtigung benötigt, kann eine andere App möglicherweise und senden Sie direkt Payload an ihn.

Berechtigung deklarieren

Die Client-App MÜSSEN die Berechtigungen in ihrer Manifest-Datei 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 privilegierte Berechtigungen, die MÜSSEN Zulassungslistendateien vorab gewährt werden. Hier sehen Sie z. B. die Datei mit der Zulassungsliste 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>

Automanager abrufen

Um die API verwenden zu können, MUSS die Client-App eine CarServiceLifecycleListener registrieren, um die zugehörigen Automanager abrufen:

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);

Discover (Absender)

Vor der Verbindung mit dem Empfängerclient SOLLTE der Absenderclient ermitteln, -Empfänger-Client durch Registrieren eines 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);
}

Vor dem Anfordern einer Verbindung zum Empfänger SOLLTE der Absender sicherstellen, Die Flags der Zone für die Belegung durch den Empfänger und der Empfänger-App sind festgelegt. 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 nur dann eine Verbindung zum Empfänger anfordert, wenn alle des Empfängers gesetzt sind. Es gibt jedoch Ausnahmen:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READY und FLAG_CLIENT_INSTALLED sind die Mindestanforderungen zum Herstellen einer Verbindung.

  • Wenn die Empfänger-App eine Benutzeroberfläche anzeigen muss, um die Verbindung, FLAG_OCCUPANT_ZONE_POWER_ON und FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED zusätzliche Anforderungen. Für eine bessere Nutzererfahrung, FLAG_CLIENT_RUNNING und FLAG_CLIENT_IN_FOREGROUND werden auch empfohlen, sonst könnte der Nutzer seien Sie überrascht.

  • Im Moment ist FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nicht implementiert (Android 15). Die Client-App kann ihn einfach ignorieren.

  • Derzeit (Android 15) unterstützt die Comms API nur mehrere Nutzer auf demselben Android-Instanz, damit Peer-Apps denselben langen Versionscode haben können (FLAG_CLIENT_SAME_LONG_VERSION) und Signatur (FLAG_CLIENT_SAME_SIGNATURE) Daher müssen Apps nicht verifizieren, dass das zwei Werte übereinstimmen.

Für eine bessere Nutzererfahrung KANN der Absender-Client eine UI anzeigen, wenn eine Markierung nicht festgelegt. Beispiel: Wenn FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nicht festgelegt ist, einen Toast oder ein Dialogfeld anzeigen, das den Nutzer auffordert, Insassenbereich des Empfängers.

Wenn der Absender die Empfänger nicht mehr ermitteln muss (z. B. wenn alle Empfänger und hergestellten Verbindungen findet oder inaktiv wird), CAN die Entdeckung zu stoppen.

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

Wenn die Erkennung angehalten wird, sind bestehende Verbindungen nicht betroffen. Der Absender kann weiterhin Payload an die verbundenen Empfänger senden.

(Sender) Anfrageverbindung

Wenn alle Flags des Empfängers festgelegt sind, kann der Absender eine Verbindung anfordern an den Empfänger:

    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 Sender eine Verbindung zum Empfänger anfordert, AbstractReceiverService in der Empfänger-App wird an den Autodienst gebunden, und AbstractReceiverService.onConnectionInitiated() werden aufgerufen. Als im Artikel Anfrageverbindung(Sender) onConnectionInitiated() ist eine abstrahierte Methode und MUSS vom Client-App.

Wenn der Empfänger die Verbindungsanfrage akzeptiert, ConnectionRequestCallback.onConnected() aufgerufen wird, dann wird die Verbindung festgelegt ist.

(Sender) Nutzlast senden

Sobald die Verbindung hergestellt ist, kann der Absender Payload an die Empfänger:

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 Payload einfügen. Wenn die andere Datentypen senden muss, MUSS er die Daten in einem Byte serialisieren Array, verwenden Sie das Byte-Array, um ein Payload-Objekt zu erstellen, und senden Sie den Parameter Payload Dann erhält der Empfängerclient das Byte-Array von der empfangenen Payload und deserialisiert das Byte-Array in das erwartete Datenobjekt. Wenn der Absender beispielsweise den String hello an den Empfänger senden möchte Endpunkt mit der ID FragmentB hat, kann mit Proto Buffers ein Datentyp definiert werden. wie hier:

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

In Abbildung 1 wird der Payload-Ablauf veranschaulicht:

Nutzlast senden

Abbildung 1: Senden Sie die Nutzlast.

(Empfängerdienst) Nutzlast empfangen und versenden

Sobald die Empfänger-App die Payload empfängt, wird AbstractReceiverService.onPayloadReceived() wird aufgerufen. Wie in den Send the Payload, onPayloadReceived() ist ein abstrahierte Methode und MUSS von der Client-App implementiert werden. Bei dieser Methode Der Client KANN Payload an die entsprechenden Empfängerendpunkte weiterleiten. Payload im Cache speichern und sie senden, sobald der erwartete Empfängerendpunkt registriert.

(Empfängerendpunkt) Registrieren und Registrierung aufheben

Die Empfänger-App SOLLTE registerReceiver() aufrufen, um den Empfänger zu registrieren. Endpunkten. Ein typischer Anwendungsfall ist, dass ein Fragment den Empfänger Payload benötigt. wird ein Empfängerendpunkt registriert:

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

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

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

Die Client-App KANN mehrere Empfängerendpunkte registrieren, receiverEndpointIds sind innerhalb der Client-App eindeutig. Das receiverEndpointId wird von AbstractReceiverService verwendet, um zu entscheiden, welcher Empfänger Endpunkt(en) zum Senden der Nutzlast. Beispiel:

  • Der Absender gibt in Payload receiver_endpoint_id:FragmentB an. Wann? beim Empfang von Payload, dem AbstractReceiverService des Empfängers forwardPayload("FragmentB", payload) zum Senden der Nutzlast an FragmentB
  • Der Absender gibt in Payload data_type:VOLUME_CONTROL an. Wann? beim Empfang der Payload weiß der AbstractReceiverService im Empfänger, dass dieser Typ von Payload an FragmentB weitergeleitet werden soll. Daher ruft er forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Absender) Verbindung beenden

Wenn der Absender Payload nicht mehr an den Empfänger senden muss, z. B. wird er inaktiv), SOLLTE die Verbindung beendet werden.

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

Wenn die Verbindung getrennt ist, 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 überprüfen Sie die entsprechenden Logs:

  1. Führen Sie für das Logging folgenden Befehl aus:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
    
  2. Zum Dump des internen Status von CarRemoteDeviceService und CarOccupantConnectionService:

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

Null CarRemoteDeviceManager und CarOccupantConnectionManager

Sieh dir diese möglichen Ursachen an:

  1. Die Autowerkstatt ist abgestürzt. Wie bereits gezeigt, sind die beiden Führungskräfte wurde absichtlich auf null zurückgesetzt, wenn der Autodienst abstürzt. Wenn der Fahrdienst neu gestartet wird, werden die beiden Manager auf Nicht-Null-Werte gesetzt.

  2. Entweder CarRemoteDeviceService oder CarOccupantConnectionService ist nicht aktiviert. Führen Sie den folgenden Befehl aus, um festzustellen, ob eine der beiden Funktionen aktiviert ist:

    adb shell dumpsys car_service --services CarFeatureController
    
    • Suchen Sie nach mDefaultEnabledFeaturesFromConfig. Es sollte Folgendes enthalten: car_remote_device_service und car_occupant_connection_service. Für 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 Multi-Display-Anzeige verwenden, MÜSSEN Sie diese Konfigurationsdatei überlagern. Sie können die beiden Dienste in einer Konfigurationsdatei:

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

Wenn die Client-App die API nicht wie vorgesehen verwendet, kann eine Ausnahme auftreten. In diesem Fall kann die Client-App die Meldung in der Ausnahme prüfen Crash-Stack, um das Problem zu beheben. Beispiele für einen Missbrauch der API:

  • registerStateCallback() Dieser Client hat bereits ein StateCallback registriert.
  • unregisterStateCallback() Von diesem/dieser Person wurde kein(e) StateCallback registriert CarRemoteDeviceManager-Instanz.
  • registerReceiver() receiverEndpointId ist bereits registriert.
  • unregisterReceiver() receiverEndpointId ist nicht registriert.
  • requestConnection() Eine ausstehende oder bestehende Verbindung ist bereits vorhanden.
  • cancelConnection() Keine ausstehende Verbindung zum Abbrechen.
  • sendPayload() Keine Verbindung hergestellt.
  • disconnect() Keine Verbindung hergestellt.

Client1 kann Nutzlasten an client2 senden, aber nicht umgekehrt.

Die Verbindung ist designbedingt eine Möglichkeit. Zum Aufbau einer bidirektionalen Verbindung müssen beide client1 und client2 MÜSSEN eine Verbindung miteinander anfordern und dann Genehmigung einholen.