Multi-Display Communications API は、AAOS のシステム特権アプリによって、車内の別の乗員ゾーンで実行されている同じアプリ(同じパッケージ名)と通信するために使用されます。このページでは、この API を統合する方法について説明します。詳しくは、CarOccupantZoneManager.OccupantZoneInfo も参照してください。
乗員ゾーン
「乗員ゾーン」のコンセプトでは、ユーザーを一連のディスプレイにマッピングします。各乗員ゾーンには、DISPLAY_TYPE_MAIN タイプのディスプレイが 1 つあります。乗員ゾーンには、クラスター ディスプレイなどの追加のディスプレイが存在する場合もあります。各乗員ゾーンには Android ユーザーが割り当てられます。各ユーザーにはそれぞれのアカウントとアプリがあります。
ハードウェア構成
Multi-Display Communications API(Comms API)は単一の SoC のみをサポートします。単一 SoC モデルでは、すべての乗員ゾーンとユーザーが同じ SoC 上で実行されます。Comms API は次の 3 つのコンポーネントで構成されています。
Power Management API により、クライアントは乗員ゾーンのディスプレイの電源を管理できます。
Discovery API により、クライアントは車内の他の乗員ゾーンの状態と、それらの乗員ゾーン内のピア クライアントをモニターできます。Discovery API は、Connection API を使用する前に使用します。
Connection API により、クライアントは別の乗員ゾーン内のピア クライアントに接続し、ピア クライアントにペイロードを送信できます。
Discovery API と Connection API は接続に必須です。Power Management API は任意です。
Comms API は異なるアプリ間の通信をサポートしていません。この 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 when
MyReceiverServicehas received a
Payloadfrom 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"/>
上記の 3 つの権限はそれぞれ特権であるため、許可リストファイルで事前に付与しなければなりません。例として、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
)を持つことができます。したがって、アプリで 2 つの値が一致することを検証する必要はありません。
ユーザー エクスペリエンスを向上させるため、送信側クライアントはフラグが設定されていない場合に 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
からバイト配列を取得し、想定されるデータ オブジェクトにバイト配列を逆シリアル化します。たとえば、送信側が ID FragmentB
の受信側エンドポイントに文字列 hello
を送信する場合は、プロトコル バッファを使用して次のようなデータ型を定義できます。
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
がクライアント アプリ内で一意である限り、複数の受信側エンドポイントを登録できます。receiverEndpointId
は、AbstractReceiverService
により、Payload のディスパッチ先の受信側エンドポイントを決定するために使用されます。次に例を示します。
- 送信側は
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
nulll の CarRemoteDeviceManager と CarOccupantConnectionManager
考えられる根本原因を調べるには:
カーサービスがクラッシュしたとします。上記のように、カーサービスがクラッシュすると、2 つのマネージャーは意図的に
null
にリセットされます。カーサービスが再起動されると、2 つのマネージャーは 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]
デフォルトでは、これら 2 つのサービスは無効になっています。デバイスがマルチディスプレイをサポートしている場合は、この構成ファイルをオーバーレイしなければなりません。次のように、構成ファイルで 2 つのサービスを有効にできます。
// 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 にペイロードを送信できるが、その逆はできない
設計上、接続は一方向のみ可能です。双方向接続を確立するには、client1
と client2
の両方が互いに接続をリクエストして、承認を得なければなりません。