Multi-Display Communications API

AAOS 中的系統特權應用程式可使用多螢幕通訊 API,與在車內不同乘客區域執行的相同應用程式 (相同套件名稱) 進行通訊。本頁面說明如何整合 API。如需更多資訊,您也可以參閱 CarOccupantZoneManager.OccupantZoneInfo

乘客區

使用者區域的概念會將使用者對應至一組螢幕。每個乘客區域都有一個型態為 DISPLAY_TYPE_MAIN 的螢幕。乘客區域也可能有其他顯示裝置,例如儀表板螢幕。每個乘客區都會指派一位 Android 使用者。每位使用者都有自己的帳戶和應用程式。

硬體設定

Comms API 僅支援單一 SoC。在單一 SoC 模型中,所有乘客區和使用者都會在同一 SoC 上執行。Comms API 包含三個元件:

  • 電源管理 API 可讓用戶端管理在使用者區域中的顯示器電源。

  • Discovery API 可讓用戶端監控車內其他乘客區域的狀態,以及監控這些乘客區域中的同類用戶端。請先使用 Discovery API,再使用 Connection API。

  • Connection API 可讓用戶端連線至其他使用者區域中的同類用戶端,並將酬載傳送至同類用戶端。

您必須使用 Discovery API 和 Connection API 才能建立連線。電源管理 API 為選用項目。

Comms API 不支援不同應用程式之間的通訊。相反地,這項功能用於同一個套件名稱的應用程式之間的通訊,且用於不同公開使用者之間的通訊。

整合指南

實作 AbstractReceiverService

如要接收 Payload,接收器應用程式必須實作 AbstractReceiverService 中定義的抽象方法。例如:

public class MyReceiverService extends AbstractReceiverService {

    @Override
    public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
    }

    @Override
    public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
            @NonNull Payload payload) {
    }
}

當傳送端用戶端要求與接收端用戶端建立連線時,系統會叫用 onConnectionInitiated()。如果需要使用者確認才能建立連線,MyReceiverService 可以覆寫這個方法,以便啟動權限活動,並根據結果呼叫 acceptConnection()rejectConnection()。否則,MyReceiverService 可以只呼叫 acceptConnection()。`

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` 覆寫此方法,以便:

  • Payload 轉送至對應的接收端點(如果有的話)。如要取得已註冊的接收器端點,請呼叫 getAllReceiverEndpoints()。如要將 Payload 轉送至指定的接收端點,請呼叫 forwardPayload()

  • Payload 快取,並在預期的接收端端點註冊時傳送,而 MyReceiverService 會透過 onReceiverRegistered() 收到通知

宣告 AbstractReceiverService

接收器應用程式務必在資訊清單檔案中宣告已實作的 AbstractReceiverService,為這項服務新增含有動作 android.car.intent.action.RECEIVER_SERVICE 的意圖篩選器,並要求 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>

android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE 權限可確保只有架構才能繫結至這項服務。如果這項服務不需要權限,其他應用程式可能就能繫結至這項服務,並直接傳送 Payload

宣告權限

用戶端應用程式必須在其資訊清單檔案中宣告權限。

<!-- 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"/>

上述三種權限都是特權權限,必須由許可清單檔案預先授予。例如,以下是 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>

取得車輛管理員

如要使用 API,用戶端應用程式必須註冊 CarServiceLifecycleListener,才能取得相關聯的 Car 管理員:

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);

(Sender) Discover

在連線至接收端用戶端之前,傳送端用戶端應註冊 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);
}

在要求連線至接收器之前,傳送端應確保已設定接收器使用者區域和接收器應用程式的所有標記。否則可能會發生錯誤。例如:

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;
}

建議您在收件者的所有標記都已設定後,再要求傳送端與收件端連線。不過,以下情況除外:

  • FLAG_OCCUPANT_ZONE_CONNECTION_READYFLAG_CLIENT_INSTALLED 是建立連線所需的最低要求。

  • 如果接收端應用程式需要顯示 UI 來取得使用者對連線的核准,則 FLAG_OCCUPANT_ZONE_POWER_ONFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED 就會成為額外需求。為了提供更好的使用者體驗,建議您也使用 FLAG_CLIENT_RUNNINGFLAG_CLIENT_IN_FOREGROUND,否則使用者可能會感到意外。

  • 目前 (Android 15) FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED 尚未實作。用戶端應用程式可以直接忽略。

  • 目前 (Android 15),Comms API 僅支援在同一個 Android 例項上使用多個使用者,這樣對等應用程式就能擁有相同的長版本代碼 (FLAG_CLIENT_SAME_LONG_VERSION) 和簽章 (FLAG_CLIENT_SAME_SIGNATURE)。因此,應用程式不需要驗證這兩個值是否一致。

為提供更好的使用者體驗,如果未設定標記,傳送端用戶端可以顯示 UI。舉例來說,如果未設定 FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED,傳送端可以顯示 Toast 或對話方塊,提示使用者解鎖接收端使用者區域的螢幕。

當傳送端不再需要探索接收端 (例如,當它找到所有接收端並建立連線或變得不活躍時),便可停止探索。

if (mRemoteDeviceManager != null) {
    mRemoteDeviceManager.unregisterStateCallback();
}

停止探索後,現有連線不會受到影響。傳送者可以繼續將 Payload 傳送至已連結的接收器。

(Sender) Request connection

當接收端的所有標記都已設定時,傳送端可以要求與接收端建立連線:

    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);
}

(接收服務) 接受連線

一旦傳送端要求與接收端建立連線,接收端應用程式中的 AbstractReceiverService 就會由車輛服務繫結,並叫用 AbstractReceiverService.onConnectionInitiated()。如(Sender) Request Connection一節所述,onConnectionInitiated() 是抽象方法,必須由用戶端應用程式實作。

接收者接受連線要求後,系統會叫用傳送者的 ConnectionRequestCallback.onConnected(),然後建立連線。

(傳送者) 傳送酬載

連線建立後,傳送者可以將 Payload 傳送給接收者:

if (mOccupantConnectionManager != null) {
    Payload payload = ...;
    try {
        mOccupantConnectionManager.sendPayload(receiverZone, payload);
    } catch (CarOccupantConnectionManager.PayloadTransferException e) {
        Log.e(TAG, "Failed to send Payload to " + receiverZone);
    }
}

寄件者可以在 Payload 中放入 Binder 物件或位元組陣列。如果傳送端需要傳送其他資料類型,則必須將資料序列化為位元組陣列,使用位元組陣列建構 Payload 物件,然後傳送 Payload。接收端用戶端會從收到的 Payload 取得位元組陣列,然後將位元組陣列反序列化為預期的資料物件。舉例來說,如果傳送端想將字串 hello 傳送至 ID 為 FragmentB 的接收端端點,可以使用 Proto Buffers 定義如下所示的資料類型:

message MyData {
  required string receiver_endpoint_id = 1;
  required string data = 2;
}

圖 1Payload 流程:

傳送酬載

圖 1. 傳送酬載。

(接收器服務) 接收及調度酬載資料

接收應用程式收到 Payload 後,系統就會叫用其 AbstractReceiverService.onPayloadReceived()。如「傳送酬載」一節所述,onPayloadReceived() 是抽象方法,且必須由用戶端應用程式實作。在此方法中,用戶端可以將 Payload 轉送至對應的接收端,也可以將 Payload 快取,然後在預期的接收端登錄後傳送。

(接收器端點) 註冊及取消註冊

接收器應用程式應呼叫 registerReceiver() 來註冊接收器端點。常見用途是 Fragment 需要接收 Payload,因此會註冊接收器端點:

private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
    …
};

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.registerReceiver("FragmentB",
                getActivity().getMainExecutor(), mPayloadCallback);
}

一旦接收端用戶端中的 AbstractReceiverServicePayload 調度至接收端端點,系統就會叫用相關聯的 PayloadCallback

只要用戶端應用程式中的 receiverEndpointId 是唯一的,用戶端應用程式就可以註冊多個接收端端點。AbstractReceiverService 會使用 receiverEndpointId 決定要將酬載傳送至哪個接收端端點。例如:

  • 傳送者在 Payload 中指定 receiver_endpoint_id:FragmentB。接收 Payload 時,接收器中的 AbstractReceiverService 會呼叫 forwardPayload("FragmentB", payload),將酬載傳送至 FragmentB
  • 傳送者在 Payload 中指定 data_type:VOLUME_CONTROL。接收 Payload 時,接收器中的 AbstractReceiverService 會知道這類 Payload 應調派至 FragmentB,因此會呼叫 forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(傳送端) 終止連線

當傳送端不再需要將 Payload 傳送至接收端 (例如變為非活動狀態) 時,應終止連線。

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.disconnect(receiverZone);
}

連線中斷後,傳送端就無法再將 Payload 傳送給接收端。

連線流程

圖 2 說明瞭連線流程。

連線流程

圖 2. 連線流程。

疑難排解

檢查記錄

如何查看對應的記錄:

  1. 執行下列指令進行記錄:

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
    
  2. 如要轉儲 CarRemoteDeviceServiceCarOccupantConnectionService 的內部狀態,請按照下列步驟操作:

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

空值 CarRemoteDeviceManager 和 CarOccupantConnectionManager

請檢查下列可能的根本原因:

  1. 車輛服務當機。如先前所述,當車輛服務發生當機時,這兩個管理員會刻意重設為 null。重新啟動車輛服務時,兩個管理員會設為非空值。

  2. CarRemoteDeviceServiceCarOccupantConnectionService 未啟用。如要判斷是否已啟用其中一個,請執行:

    adb shell dumpsys car_service --services CarFeatureController
    
    • 尋找 mDefaultEnabledFeaturesFromConfig,其中應包含 car_remote_device_servicecar_occupant_connection_service。例如:

      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]
      
    • 根據預設,這兩項服務都會停用。如果裝置支援多螢幕,您必須重疊此設定檔。您可以在設定檔中啟用這兩項服務:

      // 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>
      

呼叫 API 時發生例外狀況

如果用戶端應用程式未按照預期使用 API,可能會發生例外狀況。在這種情況下,用戶端應用程式可以檢查例外狀況和當機堆疊中的訊息,以解決問題。以下是濫用 API 的例子:

  • registerStateCallback() 這個用戶端已註冊 StateCallback
  • unregisterStateCallback() 這個 CarRemoteDeviceManager 例項未註冊任何 StateCallback
  • registerReceiver() receiverEndpointId 已註冊。
  • unregisterReceiver() receiverEndpointId 未註冊。
  • requestConnection() 已存在待處理或已建立的連線。
  • cancelConnection() 沒有待取消的連線。
  • sendPayload() 未建立連線。
  • disconnect() 未建立連線。

Client1 可以將酬載傳送至 Client2,但反之則不行

連線是單向的,如要建立雙向連線,client1client2 都必須要求彼此連線,然後取得核准。