Interfejs API Multi-Display Communications

Interfejs Multi-Display Communications API może być używany przez aplikację systemową z uprawnieniami w AAOS do komunikacji z tą samą aplikacją (o tej samej nazwie pakietu) działającą w innej strefie pasażerów w samochodzie. Na tej stronie opisujemy, jak zintegrować interfejs API. Więcej informacji znajdziesz też w artykule CarOccupantZoneManager.OccupantZoneInfo.

Strefa pasażera

Koncepcja strefy użytkownika przypisuje użytkownika do zestawu wyświetlaczy. Każda strefa pasażerska ma wyświetlacz typu DISPLAY_TYPE_MAIN. Strefa pasażera może też mieć dodatkowe wyświetlacze, np. wyświetlacz klastra. Każdej strefie zajmowanej przez pasażera przypisany jest użytkownik Androida. Każdy użytkownik ma własne konta i aplikacje.

Konfiguracja sprzętowa

Interfejs Comms API obsługuje tylko jeden SoC. W modelu z jednym SoC wszystkie strefy i użytkownicy pojazdu działają na tym samym SoC. Interfejs Comms API składa się z 3 komponentów:

  • Interfejs API zarządzania zasilaniem umożliwia klientowi zarządzanie zasilaniem wyświetlaczy w strefach pasażerów.

  • Interfejs Discovery API umożliwia klientowi monitorowanie stanów innych stref pasażerów w samochodzie oraz monitorowanie klientów równorzędnych w tych strefach. Przed użyciem interfejsu Connection API użyj interfejsu Discovery API.

  • Connection API umożliwia klientowi połączenie się z klientem równorzędnym w innej strefie użytkownika i wysłanie do niego ładunku.

Do połączenia wymagane są interfejsy Discovery API i Connection API. Interfejs Power management API jest opcjonalny.

Interfejs Comms API nie obsługuje komunikacji między różnymi aplikacjami. Jest ona przeznaczona wyłącznie do komunikacji między aplikacjami o tej samej nazwie pakietu i wyłącznie do komunikacji między różnymi widocznymi użytkownikami.

Przewodnik po integracji

Implementowanie klasy AbstractReceiverService

Aby otrzymać Payload, aplikacja odbierająca MUSI zaimplementować metody abstrakcyjne zdefiniowane w AbstractReceiverService. Przykład:

public class MyReceiverService extends AbstractReceiverService {

    @Override
    public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
    }

    @Override
    public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
            @NonNull Payload payload) {
    }
}

onConnectionInitiated() jest wywoływana, gdy klient nadawcy prosi o połączenie z tym klientem odbiorcy. Jeśli do nawiązania połączenia wymagane jest potwierdzenie użytkownika, MyReceiverService może zastąpić tę metodę, aby uruchomić aktywność związaną z uprawnieniami, i wywołać acceptConnection() lub rejectConnection() w zależności od wyniku. W przeciwnym razie MyReceiverService może po prostu zadzwonić pod numer acceptConnection().

Funkcja onPayloadReceived() jest wywoływana, gdy funkcja MyReceiverService otrzyma Payload od klienta wysyłającego. MyReceiverService może zastąpić tę metodę, aby:

  • Przekaż Payload do odpowiednich punktów końcowych odbiorcy(jeśli istnieją). Aby uzyskać zarejestrowane punkty końcowe odbiornika, wywołaj funkcję getAllReceiverEndpoints(). Aby przekazać Payload do danego punktu końcowego odbiorcy, wywołaj forwardPayload()

LUB

  • Buforuj Payload i wysyłaj go, gdy zarejestruje się oczekiwany punkt końcowy odbiorcy, o czym MyReceiverService zostanie powiadomiony przez onReceiverRegistered().

Deklarowanie klasy AbstractReceiverService

Aplikacja odbierająca MUSI zadeklarować zaimplementowany AbstractReceiverService w pliku manifestu, dodać filtr intencji z działaniem android.car.intent.action.RECEIVER_SERVICE dla tej usługi i wymagać uprawnienia android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE:

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

Uprawnienie android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE zapewnia, że tylko platforma może powiązać się z tą usługą. Jeśli ta usługa nie wymaga uprawnień, inna aplikacja może się z nią połączyć i wysłać do niej bezpośrednio Payload.

Deklarowanie uprawnień

Aplikacja kliencka MUSI zadeklarować uprawnienia w pliku manifestu.

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

Każde z tych 3 uprawnień jest uprawnieniem o podwyższonym poziomie dostępu, które MUSI zostać wstępnie przyznane przez pliki z listą dozwolonych. Oto na przykład plik listy dozwolonych aplikacji 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>

Uzyskiwanie dostępu do menedżerów samochodów

Aby korzystać z interfejsu API, aplikacja kliencka MUSI zarejestrować CarServiceLifecycleListener, aby uzyskać powiązane menedżery samochodów:

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

(Nadawca) Discover

Przed nawiązaniem połączenia z klientem odbiorcy klient nadawcy POWINIEN wykryć klienta odbiorcy, rejestrując 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);
}

Przed wysłaniem prośby o połączenie z odbiorcą nadawca POWINIEN upewnić się, że wszystkie flagi strefy zajmowanej przez odbiorcę i aplikacji odbiorcy są ustawione. W przeciwnym razie mogą wystąpić błędy. Przykład:

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

Zalecamy, aby nadawca wysyłał prośbę o połączenie do odbiorcy tylko wtedy, gdy wszystkie flagi odbiorcy są ustawione. Są jednak wyjątki:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READYFLAG_CLIENT_INSTALLED to minimalne wymagania potrzebne do nawiązania połączenia.

  • Jeśli aplikacja odbierająca musi wyświetlić interfejs, aby uzyskać zgodę użytkownika na połączenie, dodatkowymi wymaganiami stają się FLAG_OCCUPANT_ZONE_POWER_ONFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED. Aby zapewnić lepszą wygodę użytkownikom, zalecamy też używanie FLAG_CLIENT_RUNNINGFLAG_CLIENT_IN_FOREGROUND, w przeciwnym razie użytkownik może być zaskoczony.

  • Obecnie (Android 15) funkcja FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nie jest zaimplementowana. Aplikacja kliencka może po prostu zignorować ten komunikat.

  • Obecnie (Android 15) interfejs Comms API obsługuje tylko wielu użytkowników na tej samej instancji Androida, dzięki czemu aplikacje równorzędne mogą mieć ten sam długi kod wersji (FLAG_CLIENT_SAME_LONG_VERSION) i podpis (FLAG_CLIENT_SAME_SIGNATURE). W rezultacie aplikacje nie muszą sprawdzać, czy te 2 wartości są zgodne.

Aby zapewnić lepsze wrażenia użytkownika, klient nadawcy MOŻE wyświetlać interfejs, jeśli flaga nie jest ustawiona. Jeśli np. funkcja FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nie jest skonfigurowana, nadawca może wyświetlić powiadomienie lub okno, aby poprosić użytkownika o odblokowanie ekranu w strefie pasażera odbiorcy.

Gdy nadawca nie musi już wykrywać odbiorców (np. gdy znajdzie wszystkich odbiorców i nawiąże połączenia lub stanie się nieaktywny), MOŻE zakończyć wykrywanie.

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

Zatrzymanie wykrywania nie ma wpływu na istniejące połączenia. Nadawca może nadal wysyłać Payload do połączonych odbiorników.

(Nadawca) Poproś o połączenie

Gdy wszystkie flagi odbiorcy są ustawione, nadawca MOŻE poprosić o połączenie z odbiorcą:

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

(Usługa odbiorcy) Akceptowanie połączenia

Gdy nadawca poprosi o połączenie z odbiorcą, element AbstractReceiverService w aplikacji odbiorcy zostanie powiązany z usługą samochodową i wywołany zostanie element AbstractReceiverService.onConnectionInitiated(). Zgodnie z opisem w sekcji (Nadawca) Prośba o połączenie funkcja onConnectionInitiated() jest metodą abstrakcyjną i MUSI być zaimplementowana przez aplikację klienta.

Gdy odbiorca zaakceptuje prośbę o połączenie, wywoływana jest funkcja ConnectionRequestCallback.onConnected() nadawcy, a następnie nawiązywane jest połączenie.

(Nadawca) Wysyłanie ładunku

Po nawiązaniu połączenia nadawca MOŻE wysłać Payload do odbiorcy:

if (mOccupantConnectionManager != null) {
    Payload payload = ...;
    try {
        mOccupantConnectionManager.sendPayload(receiverZone, payload);
    } catch (CarOccupantConnectionManager.PayloadTransferException e) {
        Log.e(TAG, "Failed to send Payload to " + receiverZone);
    }
}

Nadawca może umieścić obiekt Binder lub tablicę bajtów w Payload. Jeśli nadawca musi wysłać inne typy danych, MUSI serializować dane do tablicy bajtów, użyć tablicy bajtów do utworzenia obiektu Payload i wysłać Payload. Następnie klient odbierający pobiera tablicę bajtów z odebranego Payloadi deserializuje ją do oczekiwanego obiektu danych. Jeśli na przykład nadawca chce wysłać ciąg znaków hello do punktu końcowego odbiorcy o identyfikatorze FragmentB, może użyć buforów protokołu do zdefiniowania typu danych w ten sposób:

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

Rysunek 1 przedstawia proces Payload:

Wysyłanie ładunku

Rysunek 1. Wyślij ładunek.

(Usługa odbiorcy) Odbieranie i wysyłanie ładunku

Gdy aplikacja odbiorcy otrzyma Payload, zostanie wywołana jej funkcja AbstractReceiverService.onPayloadReceived(). Zgodnie z opisem w sekcji Wysyłanie ładunku parametr onPayloadReceived() jest metodą abstrakcyjną i MUSI być zaimplementowany przez aplikację klienta. W tej metodzie klient MOŻE przekazywać parametr Payload do odpowiednich punktów końcowych odbiorcy lub buforować parametr Payload, a następnie wysyłać go po zarejestrowaniu oczekiwanego punktu końcowego odbiorcy.

(Punkt końcowy odbiornika) Rejestrowanie i wyrejestrowywanie

Aplikacja odbiorcy POWINNA wywołać registerReceiver(), aby zarejestrować punkty końcowe odbiorcy. Typowy przypadek użycia to sytuacja, w której fragment potrzebuje odbiornika Payload, więc rejestruje punkt końcowy odbiornika:

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

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

Gdy klient odbiorcy wyśle AbstractReceiverService do punktu końcowego odbiorcy, zostanie wywołana powiązana funkcja PayloadCallback.Payload

Aplikacja kliencka MOŻE rejestrować wiele punktów końcowych odbiorcy, o ile ich receiverEndpointIds są unikalne w ramach aplikacji klienckiej. receiverEndpointId będzie używany przez AbstractReceiverService do określania, do których punktów końcowych odbiorcy wysłać ładunek. Przykład:

  • Nadawca określa receiver_endpoint_id:FragmentBPayload. Gdy odbiornik otrzyma Payload, AbstractReceiverService w odbiorniku wywołuje forwardPayload("FragmentB", payload), aby wysłać ładunek do FragmentB.
  • Nadawca określa data_type:VOLUME_CONTROLPayload. Gdy odbiornik otrzyma Payload, AbstractReceiverService w odbiorniku wie, że ten typ Payload należy wysłać do FragmentB, więc wywołuje forwardPayload("FragmentB", payload).
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Nadawca) Zakończ połączenie

Gdy nadawca nie musi już wysyłać Payload do odbiorcy (np. gdy staje się nieaktywny), powinien zakończyć połączenie.

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

Po odłączeniu nadawca nie może już wysyłać Payload do odbiorcy.

Proces połączenia

Przepływ połączenia przedstawia rysunek 2.

Proces połączenia

Rysunek 2. Proces połączenia.

Rozwiązywanie problemów

Sprawdzanie dzienników

Aby sprawdzić odpowiednie logi:

  1. Aby włączyć rejestrowanie, uruchom to polecenie:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
  2. Aby zrzucić stan wewnętrzny CarRemoteDeviceServiceCarOccupantConnectionService:

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

Puste wartości CarRemoteDeviceManager i CarOccupantConnectionManager

Sprawdź te możliwe główne przyczyny:

  1. Usługa samochodowa uległa awarii. Jak wspomnieliśmy wcześniej, w przypadku awarii usługi samochodowej oba menedżery są celowo resetowane do wartości null. Gdy usługa samochodowa zostanie ponownie uruchomiona, oba menedżery będą miały wartości inne niż null.

  2. Właściwość CarRemoteDeviceService lub CarOccupantConnectionService nie jest włączona. Aby sprawdzić, czy jedna z tych opcji jest włączona, uruchom polecenie:

    adb shell dumpsys car_service --services CarFeatureController
    • Znajdź element mDefaultEnabledFeaturesFromConfig, który powinien zawierać elementy car_remote_device_servicecar_occupant_connection_service. Przykład:

      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]
      
    • Domyślnie te 2 usługi są wyłączone. Jeśli urządzenie obsługuje wiele wyświetlaczy, MUSISZ nałożyć ten plik konfiguracji. Możesz włączyć te 2 usługi w pliku konfiguracyjnym:

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

Wyjątek podczas wywoływania interfejsu API

Jeśli aplikacja kliencka nie używa interfejsu API zgodnie z przeznaczeniem, może wystąpić wyjątek. W takim przypadku aplikacja kliencka może sprawdzić komunikat w wyjątku i śladzie awarii, aby rozwiązać problem. Przykłady nadużywania interfejsu API:

  • registerStateCallback() Ten klient zarejestrował już StateCallback.
  • unregisterStateCallback() Ta instancja nie zarejestrowała żadnego StateCallback.CarRemoteDeviceManager
  • registerReceiver() receiverEndpointId jest już zarejestrowany.
  • unregisterReceiver() receiverEndpointId nie jest zarejestrowany.
  • requestConnection() Oczekujące lub nawiązane połączenie już istnieje.
  • cancelConnection() Brak połączenia oczekującego, które można anulować.
  • sendPayload() Brak nawiązanego połączenia.
  • disconnect() Brak nawiązanego połączenia.

Klient 1 może wysyłać ładunek do klienta 2, ale nie w drugą stronę

Połączenie jest z założenia jednokierunkowe. Aby nawiązać połączenie dwukierunkowe, zarówno client1, jak i client2 MUSZĄ wysłać do siebie prośbę o połączenie, a następnie uzyskać zgodę.