Multi-Display Communications API dapat digunakan oleh aplikasi dengan hak istimewa sistem di AAOS untuk berkomunikasi dengan aplikasi yang sama (nama paket yang sama) yang berjalan di lingkungan yang berbeda zona penumpang di dalam mobil. Halaman ini menjelaskan cara mengintegrasikan API. Untuk mempelajari Anda juga dapat melihat CarOccupantZoneManager.OccupantZoneInfo.
Zona penumpang
Konsep zona penghuni memetakan pengguna ke sekumpulan layar. Masing-masing zona penghuni memiliki layar dengan jenis DISPLAY_TYPE_MAIN. Zona penumpang juga dapat memiliki layar tambahan, seperti layar cluster. Setiap zona penumpang ditetapkan ke pengguna Android. Setiap pengguna memiliki akunnya sendiri dan aplikasi.
Konfigurasi hardware
Comms API hanya mendukung satu SoC. Pada model SoC tunggal, semua tamu zona dan pengguna berjalan di SoC yang sama. Comms API terdiri dari tiga komponen:
API pengelolaan daya memungkinkan klien mengelola daya layar di zona penumpang.
Discovery API memungkinkan klien memantau status tamu lain di dalam mobil, dan untuk memantau klien rekan di zona penumpang tersebut. Gunakan Discovery API sebelum menggunakan Connection API.
Connection API memungkinkan klien untuk terhubung ke klien peer-nya dalam zona penghuni lain dan untuk mengirim payload ke klien peer.
Discovery API dan Connection API diperlukan untuk koneksi. Kekuatan API ini bersifat opsional.
Comms API tidak mendukung komunikasi antar-aplikasi yang berbeda. Sebagai gantinya, layanan ini didesain hanya untuk komunikasi antara aplikasi dengan nama paket yang sama dan digunakan hanya untuk komunikasi antara berbagai pengguna yang terlihat.
Panduan integrasi
Mengimplementasikan AbstrakReceiverService
Untuk menerima Payload
, aplikasi penerima HARUS mengimplementasikan metode abstrak
ditentukan di AbstractReceiverService
. Contoh:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
onConnectionInitiated()
dipanggil saat klien pengirim meminta permintaan
koneksi ke klien penerima ini. Jika konfirmasi pengguna
diperlukan untuk membangun
koneksi, MyReceiverService
dapat mengganti metode ini untuk meluncurkan
aktivitas izin akses, dan memanggil acceptConnection()
atau rejectConnection()
berdasarkan
pada hasilnya. Jika tidak, MyReceiverService
bisa saja memanggil
acceptConnection()
.`
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` dapat menggantikannya
metode untuk:
- Teruskan
Payload
ke endpoint penerima yang sesuai, jika ada. Kepada dapatkan endpoint penerima yang terdaftar, panggilgetAllReceiverEndpoints()
. Kepada meneruskanPayload
ke endpoint penerima tertentu, panggilforwardPayload()
ATAU,
- Meng-cache
Payload
, dan mengirimnya saat endpoint penerima yang diharapkan terdaftar, dalam hal iniMyReceiverService
diberi tahu melaluionReceiverRegistered()
Mendeklarasikan AbstrakReceiverService
Aplikasi penerima HARUS mendeklarasikan AbstractReceiverService
yang diimplementasikan dalam
file manifes, tambahkan filter intent dengan tindakan
android.car.intent.action.RECEIVER_SERVICE
untuk layanan ini, dan memerlukan
Izin 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>
Izin android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE
memastikan bahwa hanya kerangka kerja yang
dapat mengikat ke layanan ini. Jika layanan ini
tidak memerlukan izin, aplikasi lain
mungkin bisa mengikatnya
dan mengirim Payload
secara langsung.
Mendeklarasikan izin
Aplikasi klien HARUS mendeklarasikan izin dalam file manifesnya.
<!-- 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"/>
Masing-masing dari tiga izin akses di atas adalah
izin akses istimewa, yang HARUS
yang telah diberikan sebelumnya oleh file yang diizinkan. Misalnya, berikut adalah
file daftar yang diizinkan dari
Aplikasi 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>
Mendapatkan Pengelola mobil
Untuk menggunakan API, aplikasi klien HARUS mendaftarkan CarServiceLifecycleListener
untuk
dapatkan Pengelola mobil terkait:
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);
(Pengirim) Temukan
Sebelum terhubung ke klien penerima, klien pengirim SEHARUSNYA menemukan
klien penerima dengan mendaftarkan 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);
}
Sebelum meminta koneksi ke penerima, pengirim HARUS memastikan bahwa semua flag zona penghuni penerima dan aplikasi penerima ditetapkan. Jika tidak, dapat terjadi. Contoh:
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;
}
Sebaiknya pengirim meminta koneksi ke penerima hanya jika semua penanda penerima diatur. Meskipun demikian, ada pengecualian:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
danFLAG_CLIENT_INSTALLED
adalah persyaratan minimum yang diperlukan untuk membuat koneksi.Jika aplikasi penerima perlu menampilkan UI untuk mendapatkan persetujuan pengguna
FLAG_OCCUPANT_ZONE_POWER_ON
, danFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
menjadi persyaratan tambahan. Untuk pengalaman pengguna yang lebih baik,FLAG_CLIENT_RUNNING
danFLAG_CLIENT_IN_FOREGROUND
juga direkomendasikan, jika tidak, pengguna mungkin terkejut.Untuk saat ini (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
tidak diterapkan. Aplikasi klien bisa mengabaikannya.Untuk saat ini (Android 15), Comms API hanya mendukung beberapa pengguna di Instance Android sehingga aplikasi pembanding dapat memiliki kode versi panjang yang sama (
FLAG_CLIENT_SAME_LONG_VERSION
) dan tanda tangan (FLAG_CLIENT_SAME_SIGNATURE
). Akibatnya, aplikasi tidak perlu memverifikasi bahwa dua nilai yang sama.
Untuk pengalaman pengguna yang lebih baik, klien pengirim DAPAT menampilkan UI jika tanda tidak
atur. Misalnya, jika FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
tidak ditetapkan, pengirim
dapat menampilkan toast atau dialog untuk meminta pengguna membuka kunci layar
zona penumpang penerima.
Ketika pengirim tidak perlu lagi menemukan penerima (misalnya, ketika menemukan semua penerima dan koneksi yang tersambung atau menjadi tidak aktif), CAN menghentikan penemuan.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Saat penemuan dihentikan, koneksi yang ada tidak akan terpengaruh. Pengirim dapat
terus mengirim Payload
ke penerima yang terhubung.
(Pengirim) Meminta koneksi
Jika semua tanda penerima disetel, pengirim CAN meminta koneksi ke penerima:
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);
}
(Layanan penerima) Menerima koneksi
Setelah pengirim meminta koneksi ke penerima,
AbstractReceiverService
di aplikasi penerima akan terikat oleh layanan mobil,
dan AbstractReceiverService.onConnectionInitiated()
akan dipanggil. Sebagai
yang dijelaskan dalam Koneksi Permintaan(Pengirim),
onConnectionInitiated()
adalah metode diabstraksi dan HARUS diimplementasikan oleh
aplikasi klien.
Saat penerima menerima permintaan koneksi,
ConnectionRequestCallback.onConnected()
akan dipanggil, lalu koneksi
telah ditetapkan.
(Pengirim) Mengirim payload
Setelah koneksi dibuat, pengirim DAPAT mengirim Payload
ke
penerima:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
Pengirim dapat menempatkan objek Binder
, atau array byte di Payload
. Jika
pengirim perlu mengirim tipe data lain, maka HARUS melakukan serialisasi data ke dalam byte
, gunakan array byte untuk membuat objek Payload
, lalu kirim
Payload
. Kemudian, klien penerima mendapatkan array byte dari
Payload
, dan melakukan deserialisasi array byte ke objek data yang diharapkan.
Misalnya, jika pengirim ingin mengirim String hello
ke penerima
endpoint dengan ID FragmentB
, dapat menggunakan Buffering Proto untuk menentukan jenis data
seperti ini:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
Gambar 1 mengilustrasikan alur Payload
:
(Layanan penerima) Menerima dan mengirim payload
Setelah aplikasi penerima menerima Payload
,
AbstractReceiverService.onPayloadReceived()
akan dipanggil. Seperti yang dijelaskan dalam
Kirim payload, onPayloadReceived()
adalah
metode yang diabstraksi dan HARUS diimplementasikan oleh aplikasi klien. Dalam metode ini,
klien DAPAT meneruskan Payload
ke endpoint penerima yang sesuai, atau
menyimpan Payload
ke dalam cache, lalu mengirimkannya setelah endpoint penerima yang diharapkan
terdaftar.
(Endpoint penerima) Mendaftar dan membatalkan pendaftaran
Aplikasi penerima HARUS memanggil registerReceiver()
untuk mendaftarkan penerima
endpoint. Kasus penggunaan yang umum adalah Fragment perlu menerima Payload
, jadi
ia mendaftarkan endpoint penerima:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Setelah AbstractReceiverService
di klien penerima mengirim
Payload
ke endpoint penerima, PayloadCallback
yang terkait akan
dipanggil.
Aplikasi klien DAPAT mendaftarkan beberapa endpoint penerima selama
receiverEndpointId
bersifat unik di antara aplikasi klien. receiverEndpointId
akan digunakan oleh AbstractReceiverService
untuk menentukan penerima
endpoint untuk mengirimkan Payload. Contoh:
- Pengirim menentukan
receiver_endpoint_id:FragmentB
diPayload
. Kapan menerimaPayload
,AbstractReceiverService
dalam panggilan penerimaforwardPayload("FragmentB", payload)
untuk mengirim Payload keFragmentB
- Pengirim menentukan
data_type:VOLUME_CONTROL
diPayload
. Kapan menerimaPayload
,AbstractReceiverService
di penerima akan mengetahui jenisPayload
ini harus dikirim keFragmentB
, sehingga memanggilforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Pengirim) Menghentikan koneksi
Setelah pengirim tidak perlu lagi mengirim Payload
ke penerima (misalnya,
menjadi tidak aktif), {i>router<i} HARUS mengakhiri koneksinya.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Setelah koneksi terputus, pengirim tidak dapat lagi mengirim Payload
ke penerima.
Alur koneksi
Alur koneksi diilustrasikan pada Gambar 2.
Pemecahan masalah
Periksa log
Untuk memeriksa log yang sesuai:
Jalankan perintah ini untuk logging:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
Untuk membuang status internal
CarRemoteDeviceService
danCarOccupantConnectionService
:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager dan CarOccupantConnectionManager
Lihat kemungkinan penyebab utama ini:
Layanan mobil mengalami error. Seperti yang digambarkan sebelumnya, kedua manajer itu sengaja direset ke
null
saat servis mobil mengalami error. Waktu servis mobil dimulai ulang, kedua pengelola ditetapkan ke nilai {i>non-null<i}.CarRemoteDeviceService
atauCarOccupantConnectionService
bukan mengaktifkan pembuatan versi. Untuk menentukan apakah salah satunya sudah diaktifkan, jalankan:adb shell dumpsys car_service --services CarFeatureController
Cari
mDefaultEnabledFeaturesFromConfig
, yang seharusnya berisicar_remote_device_service
dancar_occupant_connection_service
. Contoh: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]
Secara default, kedua layanan ini dinonaktifkan. Jika perangkat mendukung multi-tampilan, Anda HARUS menempatkan file konfigurasi ini di atas layar. Anda dapat mengaktifkan dua layanan dalam file konfigurasi:
// 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>
Pengecualian saat memanggil API
Jika aplikasi klien tidak menggunakan API sebagaimana mestinya, pengecualian dapat terjadi. Dalam hal ini, aplikasi klien bisa memeriksa pesan dalam pengecualian dan tumpukan error untuk menyelesaikan masalah. Contoh penyalahgunaan API adalah:
registerStateCallback()
Klien ini sudah mendaftarkanStateCallback
.unregisterStateCallback()
Tidak adaStateCallback
yang didaftarkan oleh iniCarRemoteDeviceManager
.registerReceiver()
receiverEndpointId
sudah terdaftar.unregisterReceiver()
receiverEndpointId
tidak terdaftar.requestConnection()
Koneksi yang tertunda atau dibuat sudah ada.cancelConnection()
Tidak ada koneksi tertunda untuk dibatalkan.sendPayload()
Tidak ada koneksi yang dibuat.disconnect()
Tidak ada koneksi yang dibuat.
Client1 dapat mengirim Payload ke client2, tetapi bukan sebaliknya
Koneksi bersifat satu arah berdasarkan desain. Untuk membangun koneksi
dua arah, baik
client1
dan client2
HARUS meminta koneksi satu sama lain, lalu
mendapatkan persetujuan.