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 zona penumpang yang berbeda dalam mobil. Halaman ini menjelaskan cara mengintegrasikan API. Untuk mempelajari lebih lanjut, Anda juga dapat melihat CarOccupantZoneManager.OccupantZoneInfo.
Zona penumpang
Konsep zona penghuni memetakan pengguna ke sekumpulan layar. Setiap zona hunian memiliki layar dengan jenis DISPLAY_TYPE_MAIN. Zona penghuni juga dapat memiliki layar tambahan, seperti layar cluster. Setiap zona penghuni diberi pengguna Android. Setiap pengguna memiliki akun dan aplikasinya sendiri.
Konfigurasi hardware
Comms API hanya mendukung satu SoC. Dalam model SoC tunggal, semua zona dan pengguna penumpang berjalan di SoC yang sama. Comms API terdiri dari tiga komponen:
Power management API memungkinkan klien mengelola daya tampilan di zona penumpang.
Discovery API memungkinkan klien memantau status zona penumpang lain di mobil, dan memantau klien pembanding di zona penumpang tersebut. Gunakan Discovery API sebelum menggunakan Connection API.
Connection API memungkinkan klien terhubung ke klien peer di zona penghuni lain dan mengirim payload ke klien peer.
Discovery API dan Connection API diperlukan untuk koneksi. API pengelolaan daya bersifat opsional.
Comms API tidak mendukung komunikasi antar-aplikasi. Sebagai gantinya, API ini didesain hanya untuk komunikasi antar-aplikasi dengan nama paket yang sama dan digunakan hanya untuk komunikasi antar-pengguna yang terlihat berbeda.
Panduan integrasi
Menerapkan AbstractReceiverService
Untuk menerima Payload
, aplikasi penerima HARUS menerapkan metode abstrak yang ditentukan dalam 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 koneksi ke klien penerima ini. Jika konfirmasi pengguna diperlukan untuk membuat
koneksi, MyReceiverService
dapat mengganti metode ini untuk meluncurkan
aktivitas izin, dan memanggil acceptConnection()
atau rejectConnection()
berdasarkan
hasilnya. Jika tidak, MyReceiverService
dapat langsung memanggil
acceptConnection()
.
onPayloadReceived()
dipanggil saat MyReceiverService
telah menerima
Payload
dari klien pengirim. MyReceiverService
dapat mengganti metode
ini untuk:
- Teruskan
Payload
ke endpoint penerima yang sesuai, jika ada. Untuk mendapatkan endpoint penerima yang terdaftar, panggilgetAllReceiverEndpoints()
. Untuk meneruskanPayload
ke endpoint penerima tertentu, panggilforwardPayload()
ATAU,
- Simpan
Payload
dalam cache, dan kirimkan saat endpoint penerima yang diharapkan terdaftar, yang manaMyReceiverService
akan diberi tahu melaluionReceiverRegistered()
Mendeklarasikan AbstractReceiverService
Aplikasi penerima HARUS mendeklarasikan AbstractReceiverService
yang diterapkan dalam
file manifesnya, menambahkan 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 framework yang dapat mengikat ke layanan ini. Jika layanan ini tidak memerlukan izin, aplikasi lain mungkin dapat mengikat ke layanan ini dan mengirim Payload
langsung ke layanan tersebut.
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"/>
Setiap dari tiga izin di atas adalah izin istimewa, yang HARUS
diberikan sebelumnya oleh file daftar 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
mendapatkan 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) Discover
Sebelum terhubung ke klien penerima, klien pengirim HARUS 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 semua flag zona penghuni penerima dan aplikasi penerima ditetapkan. Jika tidak, error 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 flag penerima ditetapkan. Namun, 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 atas koneksi,
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
belum diimplementasikan. Aplikasi klien dapat mengabaikannya.Untuk saat ini (Android 15), Comms API hanya mendukung beberapa pengguna di instance Android yang sama sehingga aplikasi peer dapat memiliki kode versi panjang (
FLAG_CLIENT_SAME_LONG_VERSION
) dan tanda tangan (FLAG_CLIENT_SAME_SIGNATURE
) yang sama. Akibatnya, aplikasi tidak perlu memverifikasi bahwa kedua nilai tersebut sama.
Untuk pengalaman pengguna yang lebih baik, klien pengirim DAPAT menampilkan UI jika tanda tidak disetel. Misalnya, jika FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
tidak disetel, pengirim dapat menampilkan toast atau dialog untuk meminta pengguna membuka kunci layar zona hunian penerima.
Jika pengirim tidak lagi perlu menemukan penerima (misalnya, saat pengirim menemukan semua penerima dan koneksi yang dibuat atau menjadi tidak aktif), pengirim DAPAT 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) Minta koneksi
Jika semua tanda penerima ditetapkan, pengirim DAPAT 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. Seperti
yang dijelaskan dalam (Pengirim) Minta Koneksi,
onConnectionInitiated()
adalah metode abstrak dan HARUS diterapkan oleh
aplikasi klien.
Saat penerima menerima permintaan koneksi, ConnectionRequestCallback.onConnected()
pengirim akan dipanggil, lalu koneksi akan dibuat.
(Pengirim) Kirim 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 jenis data lain, pengirim HARUS melakukan serialisasi data ke dalam array byte, menggunakan array byte untuk membuat objek Payload
, dan mengirim Payload
. Kemudian, klien penerima mendapatkan array byte dari Payload
yang diterima, dan mendeserialisasi array byte menjadi objek data yang diharapkan.
Misalnya, jika pengirim ingin mengirim String hello
ke endpoint
penerima dengan ID FragmentB
, pengirim dapat menggunakan Proto Buffers 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()
-nya akan dipanggil. Seperti yang dijelaskan dalam
Mengirim payload, onPayloadReceived()
adalah
metode abstrak dan HARUS diimplementasikan oleh aplikasi klien. Dalam metode ini, klien
DAPAT meneruskan Payload
ke endpoint penerima yang sesuai, atau
meng-cache Payload
lalu mengirimkannya setelah endpoint penerima yang diharapkan
terdaftar.
(Endpoint penerima) Mendaftarkan dan membatalkan pendaftaran
Aplikasi penerima HARUS memanggil registerReceiver()
untuk mendaftarkan endpoint penerima. Kasus penggunaan umum adalah bahwa Fragment perlu menerima Payload
, jadi
Fragment mendaftarkan endpoint penerima:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Setelah AbstractReceiverService
di klien penerima mengirimkan
Payload
ke endpoint penerima, PayloadCallback
terkait akan
dipanggil.
Aplikasi klien DAPAT mendaftarkan beberapa endpoint penerima selama receiverEndpointId
-nya unik di antara aplikasi klien. receiverEndpointId
akan digunakan oleh AbstractReceiverService
untuk memutuskan endpoint penerima mana yang akan mengirimkan Payload. Contoh:
- Pengirim menentukan
receiver_endpoint_id:FragmentB
diPayload
. Saat menerimaPayload
,AbstractReceiverService
di penerima akan memanggilforwardPayload("FragmentB", payload)
untuk mengirimkan Payload keFragmentB
- Pengirim menentukan
data_type:VOLUME_CONTROL
diPayload
. Saat menerimaPayload
,AbstractReceiverService
di penerima mengetahui bahwa 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), pengirim HARUS menghentikan koneksi.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Setelah koneksi terputus, pengirim tidak dapat lagi mengirim Payload
ke penerima.
Alur koneksi
Alur koneksi diilustrasikan dalam 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 mengekspor status internal
CarRemoteDeviceService
danCarOccupantConnectionService
:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
CarRemoteDeviceManager dan CarOccupantConnectionManager Null
Periksa kemungkinan penyebab utama berikut:
Layanan mobil error. Seperti yang diilustrasikan sebelumnya, kedua pengelola sengaja direset menjadi
null
saat layanan mobil mengalami error. Saat layanan mobil dimulai ulang, kedua pengelola akan ditetapkan ke nilai non-null.CarRemoteDeviceService
atauCarOccupantConnectionService
tidak diaktifkan. Untuk menentukan apakah salah satu atau keduanya diaktifkan, jalankan:adb shell dumpsys car_service --services CarFeatureController
Cari
mDefaultEnabledFeaturesFromConfig
, yang harus 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 atasnya. Anda dapat mengaktifkan kedua 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 dapat memeriksa pesan dalam pengecualian dan stack error untuk menyelesaikan masalah. Contoh penyalahgunaan API adalah:
registerStateCallback()
Klien ini sudah mendaftarkanStateCallback
.unregisterStateCallback()
Tidak adaStateCallback
yang didaftarkan oleh instanceCarRemoteDeviceManager
ini.registerReceiver()
receiverEndpointId
sudah terdaftar.unregisterReceiver()
receiverEndpointId
tidak terdaftar.requestConnection()
Koneksi yang menunggu persetujuan atau sudah terjalin sudah ada.cancelConnection()
Tidak ada koneksi tertunda yang dapat dibatalkan.sendPayload()
Tidak ada koneksi yang dibuat.disconnect()
Tidak ada koneksi yang dibuat.
Client1 dapat mengirim Payload ke client2, tetapi tidak sebaliknya
Koneksi ini didesain satu arah. Untuk membuat koneksi dua arah, client1
dan client2
HARUS meminta koneksi satu sama lain, lalu mendapatkan persetujuan.