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_READY
和FLAG_CLIENT_INSTALLED
是建立連線的最低需求。如果接收端應用程式需要顯示 UI,以取得使用者對連線的核准,則
FLAG_OCCUPANT_ZONE_POWER_ON
和FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
會成為額外需求。為提供更優質的使用者體驗,建議您也使用FLAG_CLIENT_RUNNING
和FLAG_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
流程:
(接收器服務) 接收及傳送酬載
接收端應用程式收到 Payload
後,就會叫用 AbstractReceiverService.onPayloadReceived()
。如「傳送酬載」一節所述,onPayloadReceived()
是抽象方法,且必須由用戶端應用程式實作。在這個方法中,用戶端可以將 Payload
轉送至對應的接收端點,或快取 Payload
,然後在註冊預期接收端點後傳送。
(接收器端點) 註冊及取消註冊
接收器應用程式「應」呼叫 registerReceiver()
,註冊接收器端點。常見的用途是 Fragment 需要接收 Payload
,因此會註冊接收器端點:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
接收端用戶端中的 AbstractReceiverService
將 Payload
傳送至接收端端點後,系統就會叫用相關聯的 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 所示。
疑難排解
檢查記錄
如要查看對應記錄,請按照下列步驟操作:
執行下列指令來記錄:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
如要傾印
CarRemoteDeviceService
和CarOccupantConnectionService
的內部狀態:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager 和 CarOccupantConnectionManager
請參閱下列可能成因:
車輛服務當機。如先前所示,當車輛服務當機時,這兩個管理員會刻意重設為
null
。重新啟動車輛服務時,這兩個管理員會設為非空值。CarRemoteDeviceService
或CarOccupantConnectionService
未啟用。如要判斷是否已啟用其中一項,請執行下列指令:adb shell dumpsys car_service --services CarFeatureController
尋找
mDefaultEnabledFeaturesFromConfig
,其中應包含car_remote_device_service
和car_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()
這個�執行個體未註冊任何StateCallback
。CarRemoteDeviceManager
registerReceiver()
receiverEndpointId
已註冊。- 「
unregisterReceiver()
」receiverEndpointId
未註冊。 requestConnection()
已經有待處理或已建立的連線。cancelConnection()
沒有待處理的連線可取消。sendPayload()
未建立連線。disconnect()
未建立連線。
Client1 可以將酬載傳送給 Client2,但反之則不行
這項連線是單向的,如要建立雙向連線,client1
和 client2
都必須向對方提出連線要求,並取得核准。