多螢幕通訊 API

AAOS 中的系統權限應用程式可以使用 Multi-Display Communications API,與車輛其他乘客區域中執行的相同應用程式 (套件名稱相同) 通訊。本頁說明如何整合 API。如要瞭解詳情,請參閱 CarOccupantZoneManager.OccupantZoneInfo

居住者區域

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

硬體設定

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

  • 電源管理 API 可讓用戶端管理居住者區域中螢幕的電源。

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

  • 連線 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()

MyReceiverService 從傳送端用戶端收到 Payload 時,系統會叫用 onPayloadReceived()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,才能取得相關聯的車輛管理員:

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

(寄件者) 探索

連線至接收端用戶端之前,傳送端用戶端應先註冊 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,傳送者可以顯示訊息或對話方塊,提示使用者解鎖接收者居住空間的螢幕。

如果傳送端不再需要探索接收端 (例如找到所有接收端並建立連線,或進入閒置狀態),可以停止探索。

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

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

(傳送者) 要求連結

當接收者的所有旗標都已設定,傳送者「可以」要求與接收者連線:

    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()。如「(傳送端) 要求連線」一節所述,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;
}

圖 1 說明 Payload 流程:

傳送酬載

圖 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),將 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

Null 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() 這個�執行個體未註冊任何 StateCallbackCarRemoteDeviceManager
  • registerReceiver() receiverEndpointId 已註冊。
  • unregisterReceiver()receiverEndpointId未註冊。
  • requestConnection() 已經有待處理或已建立的連線。
  • cancelConnection() 沒有待處理的連線可取消。
  • sendPayload() 未建立連線。
  • disconnect() 未建立連線。

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

這項連線是單向的,如要建立雙向連線,client1client2 都必須向對方提出連線要求,並取得核准。