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łajforwardPayload()
LUB
- Buforowanie
Payload
i wysyłanie go, gdy zarejestrowany jest oczekiwany punkt końcowy odbiorcy, o czymMyReceiverService
jest powiadamiany przezonReceiverRegistered()
.
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_READY
iFLAG_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_ON
iFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
. Aby zapewnić lepszą wygodę użytkownikom, zalecamy też używanieFLAG_CLIENT_RUNNING
iFLAG_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
:
(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 receiverEndpointId
s 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:FragmentB
wPayload
. Gdy odbiornik otrzymaPayload
,AbstractReceiverService
w odbiorniku wywołujeforwardPayload("FragmentB", payload)
, aby wysłać ładunek doFragmentB
. - Nadawca określa
data_type:VOLUME_CONTROL
wPayload
. Gdy odbiornik otrzymaPayload
,AbstractReceiverService
w odbiorniku wie, że ten typPayload
należy wysłać 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 (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.
Rozwiązywanie problemów
Sprawdzanie dzienników
Aby sprawdzić odpowiednie logi:
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 zrzucić stan wewnętrzny
CarRemoteDeviceService
iCarOccupantConnectionService
: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:
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.Właściwość
CarRemoteDeviceService
lubCarOccupantConnectionService
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ć elementycar_remote_device_service
icar_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 żadnegoStateCallback
.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ę.