Multi-Display Communications API

Multi-Display Communications API 可供具有特殊權限的系統應用程式使用 AAOS 用於與不同執行環境的同一應用程式 (相同的套件名稱) 通訊 乘客可能會看見車子的多個區域本頁說明如何整合 API。學習 您還能看到 CarocpantZoneManager.OccupantZoneInfo

乘客區

「佔用區域」的概念會將使用者對應至一組螢幕。每項 佔用區域有 DISPLAY_TYPE_MAIN。 佔用區域也可能有其他螢幕,例如儀表板螢幕。 每個乘客區域都會獲派一名 Android 使用者。每位使用者都有自己的帳戶 和應用程式互動

硬體設定

Comms API 僅支援一個 SoC。在單一 SoC 模型中 可用區和使用者使用同一個 SoCComms API 由三個元件組成:

  • Power management API 可讓用戶端管理 會顯示於乘客區域。

  • Discovery API 可讓用戶端監控其他乘客的狀態 以及監控這些乘客區域的對等用戶端。使用 Discovery API,然後再使用 Connection API。

  • Connection API 可讓用戶端在 以及如何將酬載傳送至對等用戶端。

必須啟用 Discovery API 和 Connection API 才能連線。力量 Management 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_SERVICEandroid.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_RUNNING和 也建議使用 FLAG_CLIENT_IN_FOREGROUND,否則使用者可能會 可能就出乎意料

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

  • Comms API 目前 (Android 15) 只支援多位使用者共用 Android 執行個體,讓對等互連應用程式可使用相同的長版代碼 (FLAG_CLIENT_SAME_LONG_VERSION) 和簽名 (FLAG_CLIENT_SAME_SIGNATURE)。因此,應用程式不需驗證 兩個值一致

為了改善使用者體驗,傳送方用戶端「CAN」會在無法標記的情況下顯示使用者介面 設定。舉例來說,如果未設定 FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED,寄件者 可以顯示浮動式訊息或對話方塊,提示使用者解鎖 接收方針區域。

寄件者不再需要找到接收端時 (例如 尋找所有接收器和既有連線,或變得閒置時),則無法 停止探索

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

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

(傳送者) 要求連線

接收者的所有旗標均已設定後,傳送方 CAN 會要求連線 的指令後:

    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) 要求連線一節所述。 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 緩衝區定義資料類型 輸入:

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 將 呼叫。

用戶端應用程式 CAN 只要能夠註冊 用戶端應用程式中的 receiverEndpointId 是不重複值。receiverEndpointId AbstractReceiverService 會使用 端點,藉此將酬載分派到哪個端點。例如:

  • 寄件者在 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. 如何傾印 CarRemoteDeviceService 的內部狀態並 CarOccupantConnectionService

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

Client1 可以將 Payload 傳送至 client2,但無法對用戶端傳送

連線方式為一種形式。如要建立雙向連線, 「client1」和「client2」必須要求建立連線,才能 獲得核准。