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.

Miejsce

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żda strefa zajmowana przez pasażera jest przypisana do użytkownika Androida. Każdy użytkownik ma własne konta i aplikacje.

Konfiguracja sprzętu

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.

  • 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 zajętości i wysłanie do niego ładunku.

Do połączenia wymagane są interfejs 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. 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) {
    }
}

onConnectionInitiated() jest wywoływana, gdy klient wysyłający żąda połączenia z tym klientem odbierającym. 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

  • Buforowanie Payload i wysyłanie go, gdy zarejestrowany jest oczekiwany punkt końcowy odbiorcy, o czym MyReceiverService jest powiadamiany 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 być 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ązanych menedżerów 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) Odkrywanie

Przed połączeniem 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. 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 wysyłał prośbę o połączenie z odbiorcą 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 na przykład wartość FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nie jest ustawiona, nadawca może wyświetlić powiadomienie lub okno, aby poprosić użytkownika o odblokowanie ekranu strefy 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 zatrzymać 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) Prośba 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ą, AbstractReceiverService w aplikacji odbiorcy zostanie powiązany z usługą samochodową, a następnie zostanie wywołana funkcja 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, zostanie wywołana funkcja ConnectionRequestCallback.onConnected() nadawcy, a następnie zostanie nawiązane 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ć tej tablicy do utworzenia obiektu Payload i wysłać Payload. Następnie klient odbiorcy pobiera tablicę bajtów z funkcji received Payload i 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 onPayloadReceived() to metoda abstrakcyjna, którą MUSI zaimplementować aplikacja kliencka. W tej metodzie klient MOŻE przekazać Payload do odpowiednich punktów końcowych odbiorcy lub zapisać Payload w pamięci podręcznej, a następnie wysł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. Na 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 stanie 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

Wartości null dla 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 otrzymają 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 to polecenie:

    adb shell dumpsys car_service --services CarFeatureController
    • Poszukaj elementu mDefaultEnabledFeaturesFromConfig, który powinien zawierać elementy car_remote_device_service i car_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ć wiadomość w wyjątku i śladzie awarii, aby rozwiązać problem. Przykłady niewłaściwego korzystania z 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() Istnieje już oczekujące lub nawiązane połączenie.
  • cancelConnection() Brak oczekującego połączenia do anulowania.
  • 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ę.