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 when
MyReceiverServicehas received a
Payloadfrom 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óregoMyReceiverService
zostanie powiadomiony przezonReceiverRegistered()
.
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
iFLAG_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_ON
iFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
. Dla lepszego komfortu użytkownika zalecamy też użycie elementówFLAG_CLIENT_RUNNING
iFLAG_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
:
(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 poluPayload
. Podczas odbieraniaPayload
AbstractReceiverService
w urządzeniu odbiorczym wywołujeforwardPayload("FragmentB", payload)
, aby wysłać Payload doFragmentB
. - Nadawca podaje
data_type:VOLUME_CONTROL
w poluPayload
. Podczas odbieraniaPayload
AbstractReceiverService
w odbiornikach wie, że ten typPayload
powinien zostać wysłany doFragmentB
, więc wywołujeforwardPayload("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.
Rozwiązywanie problemów
Sprawdzanie dzienników
Aby sprawdzić odpowiednie dzienniki:
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"
Aby wyodrębnić stan wewnętrzny
CarRemoteDeviceService
iCarOccupantConnectionService
:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager i CarOccupantConnectionManager
Możliwe przyczyny tego problemu:
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.Nie włączono funkcji
CarRemoteDeviceService
aniCarOccupantConnectionService
. 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ć polacar_remote_device_service
icar_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 instancjaCarRemoteDeviceManager
nie zarejestrowała żadnegoStateCallback
.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, client1
i client2
muszą poprosić o połączenie, a następnie uzyskać na nie zgodę.