Interfejs Multi-Display Communications API

Interfejs API do komunikacji z wielu wyświetlaczami może być używany przez aplikację z przywilejami systemowymi w AAOS do komunikacji z tą samą aplikacją (o tej samej nazwie pakietu) działającą w innej strefie pasażera w samochodzie. Na tej stronie opisaliśmy, jak zintegrować interfejs API. Więcej informacji znajdziesz też w CarOccupantZoneManager.OccupantZoneInfo.

Miejsce

Koncepcja strefy pasażera przypisuje użytkownika do zestawu wyświetlaczy. Każda strefa pasażera ma wyświetlacz typu DISPLAY_TYPE_MAIN. Strefa pasażera może też zawierać dodatkowe wyświetlacze, takie jak wyświetlacz klastra. Każdej strefie 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 przypadku modelu z jednym SoC wszystkie strefy i użytkownicy pasażerów działają na tym samym SoC. Interfejs Comms API składa się z 3 komponentów:

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

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

  • Connection API umożliwia klientowi nawiązanie połączenia z klientem peer w innej strefie lokacyjnej i wysłanie ładunku do tego klienta.

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

Interfejs API Comms nie obsługuje komunikacji między różnymi aplikacjami. Zamiast tego jest ona przeznaczona tylko do komunikacji między aplikacjami o tej samej nazwie pakietu i używana tylko do komunikacji między różnymi widocznymi użytkownikami.

Przewodnik integracji

Implementacja AbstractReceiverService

Aby otrzymać Payload, aplikacja odbiorcza MUSI zaimplementować abstrakcyjne metody zdefiniowane w AbstractReceiverService. Może to obejmować np. te funkcje:

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ływany, gdy klient nadawcy żąda połączenia z tym klientem odbiorcy. Jeśli do nawiązania połączenia potrzebne jest potwierdzenie użytkownika, MyReceiverService może zastąpić tę metodę, aby uruchomić działanie związane z uzyskiwaniem uprawnień, i wywołać metodę acceptConnection() lub rejectConnection() w zależności od wyniku. W przeciwnym razie MyReceiverService może po prostu zadzwonić do acceptConnection().`

Metoda onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService może zastąpić tę metodę, aby:

  • Przekieruj 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 określonego punktu końcowego odbiorcy, wywołaj funkcję forwardPayload()

LUB

  • Zapisz w pamięci podręcznej Payload i wyślij go, gdy zarejestrowany zostanie oczekiwany punkt końcowy odbiorcy, dla którego MyReceiverService zostanie powiadomiony przez onReceiverRegistered().

Zadeklaruj abstrakcyjną usługę AbstractReceiverService

Aplikacja odbiornika MUSI zadeklarować zaimplementowaną usługę 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 się powiązać z tą usługą. Jeśli ta usługa nie wymaga uprawnień, inna aplikacja może się z nią połączyć i bezpośrednio wysłać do niej 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 3 wymienionych wyżej uprawnień jest uprawnieniem uprzywilejowanym, które MUSI być przyznane przez pliki na liście dozwolonych. Oto na przykład plik allowlist 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>

Menedżerowie Car

Aby korzystać z interfejsu API, aplikacja klienta 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) Discover

Przed nawiązaniem połączenia z odbiorcą klient nadawczy POWINIEN wykryć klienta odbiorczego, 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 poprosi o połączenie z odbiornikiem, nadawca POWINIEN upewnić się, że wszystkie flagi strefy pasażera odbiornika i aplikacji odbiornika są ustawione. W przeciwnym razie mogą wystąpić błędy. Może to obejmować np. te funkcje:

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 poprosił o połączenie z odbiorcą tylko wtedy, gdy wszystkie flagi odbiorcy są ustawione. Są jednak wyjątki:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READY i FLAG_CLIENT_INSTALLED to minimalne wymagania potrzebne do nawiązania połączenia.

  • Jeśli aplikacja odbiorcy musi wyświetlić interfejs, aby uzyskać zgodę użytkownika na połączenie, wymagania dodatkowe to FLAG_OCCUPANT_ZONE_POWER_ONFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED. Dla lepszego komfortu użytkownika zalecamy też użycie elementów FLAG_CLIENT_RUNNINGFLAG_CLIENT_IN_FOREGROUND, ponieważ w przeciwnym razie może on być zaskoczony.

  • Obecnie (Android 15) funkcja FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nie jest jeszcze dostępna. Aplikacja klienta może je zignorować.

  • Obecnie (Android 15) interfejs API Comms obsługuje tylko wielu użytkowników w tym samym wystąpieniu Androida, aby aplikacje na tym samym poziomie mogły mieć ten sam długi kod wersji (FLAG_CLIENT_SAME_LONG_VERSION) i tą samą sygnaturę (FLAG_CLIENT_SAME_SIGNATURE). W rezultacie aplikacje nie muszą weryfikować, czy te 2 wartości są zgodne.

Aby zapewnić użytkownikom lepsze wrażenia, klient nadawcy MOŻE wyświetlić interfejs użytkownika, jeśli flaga nie jest ustawiona. Jeśli na przykład FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED nie jest ustawiona, nadawca może wyświetlić komunikat lub okno dialogowe, aby poprosić użytkownika o odblokowanie ekranu w strefie pasażera.

Gdy nadawca nie musi już wykrywać odbiorców (na przykład gdy znajdzie wszystkich odbiorców i ustanowi połączenia lub stanie się nieaktywny), MOŻE przerwać 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 odbiorcó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ą, usługa samochodowa zwiąże się z elementem AbstractReceiverService w aplikacji odbiorcy, a następnie wywoła element AbstractReceiverService.onConnectionInitiated(). Jak wyjaśniono w sekcji (Wysyłanie) Prośba o połączenie, metoda 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ć w Payload obiekt Binder lub tablicę bajtów. Jeśli nadawca musi wysłać inne typy danych, musi je zserializować w tablicy bajtów, użyć tablicy bajtów do utworzenia obiektu Payload i wysłać obiekt Payload. Następnie klient odbiorczy pobiera tablicę bajtów z otrzymanego zapytania Payload i deserializuje ją w oczekiwanym obiekcie danych. Jeśli na przykład nadawca chce wysłać ciąg znaków hello do odbiorczego punktu końcowego o identyfikatorze FragmentB, może użyć Proto Buffers 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. Prześlij ładunek.

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

Gdy aplikacja odbiorcy otrzyma Payload, zostanie wywołana jej metoda AbstractReceiverService.onPayloadReceived(). Jak wyjaśniono w sekcji Wysyłanie ładunku, onPayloadReceived() to metoda abstrakcyjna, którą MUSI zaimplementować aplikacja kliencka. W ramach tej metody klient MOŻE przekazać Payload do odpowiednich punktów końcowych odbiorcy lub zapisać Payload w pamięci podręcznej, a potem wysłać go po zarejestrowaniu oczekiwanego punktu końcowego odbiorcy.

(Punkt końcowy odbiorcy) Rejestracja i wyrejestrowanie

Aplikacja odbiorcy powinna wywołać funkcję registerReceiver(), aby zarejestrować punkty końcowe odbiorcy. Typowym przypadkiem użycia jest sytuacja, w której fragment musi odbierać dane z adresu Payload, więc rejestruje punkt końcowy odbiorczy:

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

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

Gdy AbstractReceiverService w kliencie odbiorczym prześle Payload do punktu końcowego odbiorczego, zostanie wywołany powiązany element PayloadCallback.

Aplikacja klienta MOŻE zarejestrować wiele punktów końcowych odbiorczych, o ile ich identyfikatory receiverEndpointId są unikalne w aplikacji klienta. Identyfikator receiverEndpointId będzie używany przez AbstractReceiverService do podejmowania decyzji, do których punktów końcowych odbiorczych wysłać Payload. Może to obejmować np. te funkcje:

  • Nadawca podaje receiver_endpoint_id:FragmentB w polu Payload. Podczas odbierania Payload AbstractReceiverService w urządzeniu odbiorczym wywołuje forwardPayload("FragmentB", payload), aby wysłać Payload do FragmentB.
  • Nadawca podaje data_type:VOLUME_CONTROL w polu Payload. Podczas odbierania Payload AbstractReceiverService w odbiornikach 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 musi już wysyłać Payload do odbiorcy (na przykład gdy staje się on nieaktywny), powinien zakończyć połączenie.

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

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

Proces łączenia

Proces łączenia przedstawia rysunek 2.

Proces łączenia

Rysunek 2. Proces łączenia.

Rozwiązywanie problemów

Sprawdzanie dzienników

Aby sprawdzić odpowiednie dzienniki:

  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 wyodrębnić stan wewnętrzny CarRemoteDeviceServiceCarOccupantConnectionService:

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

Null CarRemoteDeviceManager i CarOccupantConnectionManager

Możliwe przyczyny tego problemu:

  1. Usługa samochodowa uległa awarii. Jak już wspomniano, gdy usługa samochodowa ulegnie awarii, null są celowo resetowane. Gdy usługa car service jest ponownie uruchamiana, oba menedżery otrzymują wartości niezerowe.

  2. Nie włączono funkcji CarRemoteDeviceService ani CarOccupantConnectionService. Aby sprawdzić, czy jedno z tych ustawień jest włączone, uruchom:

    adb shell dumpsys car_service --services CarFeatureController
    
    • Poszukaj pola mDefaultEnabledFeaturesFromConfig, które powinno zawierać pola car_remote_device_service i car_occupant_connection_service. Na 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 wyświetlacze, 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 klienta nie korzysta z interfejsu API zgodnie z przeznaczeniem, może wystąpić wyjątek. W takim przypadku aplikacja klienta może sprawdzić komunikat w wyjątkach i zbiorze wyjątków, aby rozwiązać problem. Przykłady niewłaściwego użycia interfejsu API:

  • registerStateCallback() Ten klient ma już zarejestrowaną domenę StateCallback.
  • unregisterStateCallback() Ta instancja CarRemoteDeviceManager nie zarejestrowała żadnego StateCallback.
  • registerReceiver() receiverEndpointId jest już zarejestrowany.
  • unregisterReceiver() receiverEndpointId nie jest zarejestrowany.
  • requestConnection() Połączenie oczekujące lub ustanowione już istnieje.
  • cancelConnection() Brak oczekujących połączeń do anulowania.
  • sendPayload() Brak połączenia.
  • disconnect() Brak połączenia.

Klient1 może wysyłać ładunek do klienta2, ale nie odwrotnie

Połączenie jest jednokierunkowe. Aby nawiązać połączenie dwukierunkowe, client1client2 muszą poprosić o połączenie, a następnie uzyskać na nie zgodę.