Multi-Display Communications API

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 di mobil. Halaman ini menjelaskan cara mengintegrasikan API. Untuk mempelajari lebih lanjut, Anda juga dapat melihat CarOccupantZoneManager.OccupantZoneInfo.

Zona penumpang

Konsep zona penumpang memetakan pengguna ke serangkaian layar. Setiap zona penumpang memiliki layar dengan jenis DISPLAY_TYPE_MAIN. Zona penumpang juga dapat memiliki layar tambahan, seperti layar cluster. Setiap zona penumpang ditetapkan pengguna Android. Setiap pengguna memiliki akun dan aplikasinya sendiri.

Konfigurasi hardware

Comms API hanya mendukung satu SoC. Dalam model SoC tunggal, semua zona penumpang dan pengguna berjalan di SoC yang sama. Comms API terdiri dari tiga komponen:

  • Power management API memungkinkan klien mengelola daya layar di zona penghuni.

  • Discovery API memungkinkan klien memantau status zona penumpang lain di mobil, dan memantau klien sejawat di zona penumpang tersebut. Gunakan Discovery API sebelum menggunakan Connection API.

  • Connection API memungkinkan klien terhubung ke klien peer-nya di zona penghuni lain dan mengirim payload ke klien peer.

Discovery API dan Connection API diperlukan untuk koneksi. API manajemen Power adalah opsional.

Comms API tidak mendukung komunikasi antar-aplikasi yang berbeda. Sebagai gantinya, API ini hanya dirancang untuk komunikasi antar-aplikasi dengan nama paket yang sama dan hanya digunakan untuk komunikasi antar-pengguna yang terlihat berbeda.

Panduan integrasi

Mengimplementasikan 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 memanggil acceptConnection().`

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` dapat mengganti metode ini menjadi:

  • Teruskan Payload ke endpoint penerima yang sesuai, jika ada. Untuk mendapatkan endpoint penerima terdaftar, panggil getAllReceiverEndpoints(). Untuk meneruskan Payload ke endpoint penerima tertentu, panggil forwardPayload()

ATAU,

  • Simpan Payload dalam cache, dan kirimkan saat endpoint penerima yang diharapkan terdaftar, yang MyReceiverService-nya diberi tahu melalui onReceiverRegistered()

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 mewajibkan 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 terikat ke layanan ini 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"/>

Ketiga izin di atas adalah izin dengan hak istimewa, yang HARUS diberikan sebelumnya oleh file daftar yang diizinkan. Misalnya, berikut adalah file daftar yang diizinkan 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 tanda zona penghuni penerima dan aplikasi penerima telah 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 tanda penerima telah ditetapkan. Namun, ada pengecualian:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READY dan FLAG_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 dan FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED menjadi persyaratan tambahan. Untuk pengalaman pengguna yang lebih baik, FLAG_CLIENT_RUNNING dan FLAG_CLIENT_IN_FOREGROUND juga direkomendasikan. Jika tidak, pengguna mungkin terkejut.

  • Untuk saat ini (Android 15), FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED tidak diimplementasikan. Aplikasi klien dapat mengabaikannya.

  • Untuk saat ini (Android 15), Comms API hanya mendukung beberapa pengguna pada 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 dua nilai tersebut sama.

Untuk pengalaman pengguna yang lebih baik, klien pengirim DAPAT menampilkan UI jika tanda tidak ditetapkan. Misalnya, jika FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED tidak disetel, pengirim dapat menampilkan toast atau dialog untuk meminta pengguna membuka kunci layar zona penumpang penerima.

Jika pengirim tidak lagi perlu menemukan penerima (misalnya, saat 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) Meminta koneksi

Jika semua flag 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) Meminta 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) 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 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:

Mengirim Payload

Gambar 1. Kirim 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 menyimpan Payload dalam cache, 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 yang umum adalah bahwa Fragment perlu menerima Payload, sehingga 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 terkait akan dipanggil.

Aplikasi klien DAPAT mendaftarkan beberapa endpoint penerima selama receiverEndpointId-nya unik di antara aplikasi klien. receiverEndpointId akan digunakan oleh AbstractReceiverService untuk menentukan endpoint penerima mana yang akan menerima Payload. Contoh:

  • Pengirim menentukan receiver_endpoint_id:FragmentB di Payload. Saat menerima Payload, AbstractReceiverService di penerima akan memanggil forwardPayload("FragmentB", payload) untuk mengirim Payload ke FragmentB
  • Pengirim menentukan data_type:VOLUME_CONTROL di Payload. Saat menerima Payload, AbstractReceiverService di penerima mengetahui bahwa jenis Payload ini harus dikirim ke FragmentB, sehingga memanggil forwardPayload("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 terputus, pengirim tidak dapat lagi mengirim Payload ke penerima.

Alur koneksi

Alur koneksi diilustrasikan dalam Gambar 2.

Alur koneksi

Gambar 2. Alur koneksi.

Pemecahan masalah

Memeriksa log

Untuk memeriksa log yang sesuai:

  1. 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"
    
  2. Untuk membuang status internal CarRemoteDeviceService dan CarOccupantConnectionService:

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

Null CarRemoteDeviceManager dan CarOccupantConnectionManager

Lihat kemungkinan penyebab utamanya berikut:

  1. Layanan mobil error. Seperti yang diilustrasikan sebelumnya, kedua pengelola disetel ulang secara sengaja menjadi null saat layanan mobil mengalami error. Saat layanan mobil dimulai ulang, kedua pengelola ditetapkan ke nilai non-null.

  2. CarRemoteDeviceService atau CarOccupantConnectionService tidak diaktifkan. Untuk menentukan apakah salah satu diaktifkan, jalankan:

    adb shell dumpsys car_service --services CarFeatureController
    
    • Cari mDefaultEnabledFeaturesFromConfig, yang harus berisi car_remote_device_service dan car_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-layar, Anda HARUS menempatkan file konfigurasi ini. 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 seperti yang diinginkan, 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 mendaftarkan StateCallback.
  • unregisterStateCallback() Tidak ada StateCallback yang terdaftar oleh instance CarRemoteDeviceManager ini.
  • registerReceiver() receiverEndpointId sudah terdaftar.
  • unregisterReceiver() receiverEndpointId tidak terdaftar.
  • requestConnection() Koneksi yang tertunda atau sudah dibuat sudah ada.
  • cancelConnection() Tidak ada koneksi yang tertunda untuk dibatalkan.
  • sendPayload() Tidak ada koneksi yang dibuat.
  • disconnect() Tidak ada koneksi yang dibuat.

Client1 dapat mengirim Payload ke client2, tetapi tidak sebaliknya

Koneksi ini bersifat satu arah sesuai dengan desain. Untuk membuat koneksi dua arah, baik client1 maupun client2 HARUS meminta koneksi satu sama lain, lalu mendapatkan persetujuan.