Interfejs Multi-Display Communications API

Interfejsu Multi-Display Communications API może używać w Komunikacja z tą samą aplikacją (ta sama nazwa pakietu) w innym systemie AAOS w strefie podróży w samochodzie. Na tej stronie dowiesz się, jak zintegrować interfejs API. Aby się uczyć możesz też zobaczyć CarOccupantZoneManager.OccupantZoneInfo,

Strefa użytkowników

Pojęcie strefy zajętości przypisuje użytkownika do zestawu wyświetlaczy. Każdy w strefie osób jest wyświetlany typ DISPLAY_TYPE_MAIN. Strefa dla gości może też mieć dodatkowe ekrany, na przykład wyświetlacz klastra. Do każdej strefy jest przypisany użytkownik Androida. Każdy użytkownik ma własne konto i aplikacje.

Konfiguracja sprzętowa

Comms API obsługuje tylko jeden układ SOC. W modelu z pojedynczym układem SOC strefy i użytkownicy korzystają z tego samego układu SOC. Interfejs Comms API składa się z 3 komponentów:

  • Power Management API pozwala klientowi zarządzać w strefach użytkowników.

  • Discovery API umożliwia klientowi monitorowanie stanów innych osób w samochodzie oraz do monitorowania klientów równorzędnych w tych strefach. Używaj Discovery API przed użyciem Connection API.

  • Interfejs Connection API pozwala klientowi nawiązać połączenie z klientem równorzędnym w do kolejnej strefy zajętości, aby wysłać ładunek do klienta równorzędnego.

Do połączenia wymagane są interfejsy Discovery API i Connection API. Moc API do zarządzania jest opcjonalny.

Interfejs Comms API nie obsługuje komunikacji między różnymi aplikacjami. Zamiast tego: służy tylko do komunikacji między aplikacjami o tej samej nazwie pakietu i używany tylko do komunikacji między różnymi widocznymi użytkownikami.

Przewodnik po integracji

Implementowanie usługi AbstractReceiverService

Aby otrzymać Payload, aplikacja odbierająca MUSI obsługiwać metody abstrakcyjne zdefiniowane w: AbstractReceiverService. Na przykład:

public class MyReceiverService extends AbstractReceiverService {

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

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

Funkcja onConnectionInitiated() jest wywoływana, gdy klient nadawcy zażąda żądania z tym klientem odbierającym. Jeśli konieczne jest potwierdzenie przez użytkownika, połączenia, MyReceiverService może zastąpić tę metodę, aby uruchomić działania związane z uprawnieniami i wywoływanie w oparciu o acceptConnection() lub rejectConnection() na temat wyniku. W przeciwnym razie MyReceiverService może zadzwonić po prostu acceptConnection().

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` może zastąpić to pole :

  • Przekieruj Payload do odpowiednich punktów końcowych odbiorcy(jeśli występują). Do pobierz zarejestrowane punkty końcowe odbiorcy, wywołaj getAllReceiverEndpoints(). Do przekierowanie Payload do danego punktu końcowego odbiorcy, wywołanie forwardPayload()

LUB

  • Zapisz w pamięci podręcznej Payload i wysyłaj go, gdy oczekiwany punkt końcowy odbiorcy będzie zarejestrowanych, w przypadku których domena MyReceiverService jest powiadamiana przez onReceiverRegistered()

Deklarowanie usługi AbstractReceiverService

Aplikacja odbierająca MUSI zadeklarować zaimplementowany interfejs AbstractReceiverService w swoich plik manifestu, dodaj filtr intencji z działaniem android.car.intent.action.RECEIVER_SERVICE dla tej usługi i wymagają Uprawnienie 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>

Uprawnienia android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE gwarantuje, że tylko platforma może powiązać z tą usługą. Jeśli ta usługa nie wymaga tych uprawnień, może je powiązać inna aplikacja i wyślij do niej Payload.

Zadeklaruj uprawnienia

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 trzech powyższych uprawnień to uprawnienia uprzywilejowane, które MUSZĄ być wstępnie przyznane przez pliki z listy dozwolonych. Oto lista dozwolonych plików: Aplikacja 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>

Pobieranie menedżerów samochodu

Aby używać interfejsu API, aplikacja kliencka MUSI zarejestrować CarServiceLifecycleListener w wyświetlić powiązanych menedżerów samochodu:

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 połączeniem się z klientem odbierającym klient wysyłający POWINIEN wykryć klienta odbierającego, 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);
}

Zanim poprosisz o połączenie z odbiorcą, nadawca POWINIEN upewnić się, że wszystkie ustawione są flagi strefy docelowej i aplikacji odbierającej. W przeciwnym razie . Na 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 prosił o połączenie z odbiorcą tylko wtedy, gdy wszystkie flagi odbiorcy. Istnieją jednak wyjątki:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READY i FLAG_CLIENT_INSTALLED to minimalne wymagania dotyczące nawiązywania połączenia.

  • Jeśli aplikacja odbierająca musi wyświetlać interfejs, aby uzyskać zgodę użytkownika połączenie, FLAG_OCCUPANT_ZONE_POWER_ON i FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED stają się dodatkowymi wymaganiami. Dla lepsze wrażenia użytkowników, FLAG_CLIENT_RUNNING i Zalecane są również FLAG_CLIENT_IN_FOREGROUND. W przeciwnym razie użytkownik może dziwić się.

  • Aplikacja FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nie jest obecnie zaimplementowana (Android 15). Aplikacja kliencka może go po prostu zignorować.

  • Obecnie (Android 15) interfejs Comms API obsługuje tylko wielu użytkowników instancję 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 efekcie aplikacje nie muszą sprawdzać, czy zgadzają się dwie wartości.

Dla wygody użytkowników klient nadawcy może wyświetlać interfejs, jeśli flaga nie jest ustawiony. Jeśli na przykład FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nie jest ustawiony, nadawca może wyświetlać tost lub okno z prośbą o odblokowanie ekranu w strefie odbiornika.

Gdy nadawca nie musi już wykrywać odbiorców (na przykład znajduje wszystkich odbiorców i nawiązane połączenia lub przestaje być aktywny), CAN i zatrzymać odkrywanie.

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

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

(Nadawca) Prośba o połączenie

Gdy ustawione są wszystkie flagi odbiorcy, nadawca może poprosić o połączenie do odbiorcy:

    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 odbiornika) Zaakceptuj połączenie

Gdy nadawca poprosi o połączenie z odbiorcą, Adres AbstractReceiverService w aplikacji odbiornika będzie powiązany z usługą samochodową, i AbstractReceiverService.onConnectionInitiated(). Jako omówione w artykule Wysyłanie żądania(nadawca), onConnectionInitiated() to metoda abstrakcyjna i MUSI być zaimplementowana przez aplikacji klienckiej.

Gdy odbiorca zaakceptuje żądanie połączenia, Zostanie wywołana funkcja ConnectionRequestCallback.onConnected(), a następnie połączenie

(Nadawca) Wyślij ładunek

Po nawiązaniu połączenia nadawca może wysłać wiadomość Payload do odbiorca:

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 obiekcie Payload. Jeśli nadawca musi wysyłać inne typy danych, MUSI zserializować dane do bajta , użyj tablicy bajtów do utworzenia obiektu Payload i wyślij Payload Następnie klient odbierający pobiera tablicę bajtów z odebranego Payload, i przekształca tablicę bajtów w oczekiwany obiekt danych. Jeśli na przykład nadawca chce wysłać ciąg znaków hello do odbiorcy punktu końcowego o identyfikatorze FragmentB, może użyć buforów Proto, aby określić typ danych podobny do tego:

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

Rysunek 1 przedstawia proces Payload:

Wyślij ładunek

Rysunek 1. Wyślij ładunek.

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

Gdy aplikacja odbierająca otrzyma Payload, Zostanie wywołana funkcja AbstractReceiverService.onPayloadReceived(). Jak wyjaśniono w Wyślij ładunek, onPayloadReceived() to to metoda abstrakcyjna i MUSI być wdrażana przez aplikację kliencką. W tej metodzie klient MOŻE przekazać Payload do odpowiednich punktów końcowych odbiorcy lub zapisz w pamięci podręcznej Payload, a następnie wyślij go, gdy oczekiwany punkt końcowy odbiorcy będzie zarejestrowano.

(Punkt końcowy odbiorcy) Zarejestruj i wyrejestruj

Aplikacja odbierająca POWINNA wywołać metodę registerReceiver(), by zarejestrować odbiorcę i punktów końcowych. Typowym przypadkiem użycia jest to, że fragment kodu musi odbierać element Payload, więc rejestruje punkt końcowy odbiorcy:

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

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

Gdy AbstractReceiverService w kliencie odbierającym wyśle Payload do punktu końcowego odbiorcy, powiązane PayloadCallback będzie .

Aplikacja kliencka może rejestrować wiele punktów końcowych odbiorcy, o ile ich Elementy typu receiverEndpointId są unikalne w poszczególnych aplikacjach klienckich. receiverEndpointId zostaną użyte przez aplikację AbstractReceiverService do określenia, który odbiornik punkty końcowe, do których ma być wysyłane ładunek. Na przykład:

  • Nadawca określa receiver_endpoint_id:FragmentB w Payload. Kiedy odbieranie komunikatów Payload, AbstractReceiverService w połączeniach odbierających forwardPayload("FragmentB", payload) w celu wysłania ładunku do FragmentB
  • Nadawca określa data_type:VOLUME_CONTROL w Payload. Kiedy odbiera wiadomość Payload, AbstractReceiverService w odbiorniku wie że ten typ Payload powinien zostać wysłany do FragmentB, więc wywołuje forwardPayload("FragmentB", payload)
.
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(Nadawca) Zakończ połączenie

Gdy nadawca nie będzie już musiał wysyłać wiadomości Payload do odbiorcy (na przykład stanie się nieaktywne), POWINNA SIĘ zakończyć połączenie.

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

Po rozłączeniu nadawca nie będzie mógł wysyłać wiadomości Payload do odbiorcy.

Przepływ połączenia

Przepływ połączeń został przedstawiony na Rys. 2.

Przepływ połączenia

Rysunek 2. Przepływ połączeń.

Rozwiązywanie problemów

Sprawdzanie dzienników

Aby sprawdzić odpowiednie dzienniki:

  1. Uruchom to polecenie, aby rozpocząć rejestrowanie:

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

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

Null CarRemoteDeviceManager i CarOccupantConnectionManager

Sprawdź te potencjalne główne przyczyny:

  1. Usługa samochodowa uległa awarii. Jak widać na przykładzie, 2 menedżerowie celowo resetuje się do ustawienia null w przypadku wypadku w serwisie samochodowym. Gdy warsztat samochodowy po zrestartowaniu, oba menedżery mają niepuste wartości.

  2. CarRemoteDeviceService albo CarOccupantConnectionService nie są . Aby sprawdzić, czy któraś z nich jest włączona, uruchom polecenie:

    adb shell dumpsys car_service --services CarFeatureController
    
    • Odszukaj mDefaultEnabledFeaturesFromConfig, który powinien zawierać car_remote_device_service i car_occupant_connection_service. Dla: 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. Gdy urządzenie obsługuje na wiele wyświetlaczy, MUSISZ nałożyć ten plik konfiguracji. Możesz włączyć te 2 usługi w pliku konfiguracji:

      // 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 w oczekiwany sposób, może wystąpić wyjątek. W takim przypadku aplikacja kliencka może sprawdzić wiadomość w wyjątku oraz stosu awarii, aby rozwiązać problem. Przykłady nadużyć interfejsu API:

  • registerStateCallback() Ten klient zarejestrował już StateCallback.
  • unregisterStateCallback() Nie zarejestrowano przez to: StateCallback CarRemoteDeviceManager instancję.
  • registerReceiver() receiverEndpointId jest już zarejestrowany.
  • unregisterReceiver() receiverEndpointId nie jest zarejestrowany.
  • requestConnection() Oczekujące lub nawiązane połączenie już istnieje.
  • cancelConnection() Brak oczekujących połączeń do anulowania.
  • sendPayload() Brak nawiązanego połączenia.
  • disconnect() Brak nawiązanego połączenia.

Klient1 może wysłać ładunek do klienta 2, ale nie na odwrót

Połączenie jest zamierzone. Aby nawiązać połączenie dwukierunkowe, zarówno client1 i client2 MUSZĄ poprosić o połączenie, a potem uzyskać zgodę.