Multi-Display Communications API

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

上記の 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_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)を持つことができます。したがって、アプリで 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);
    }
}

送信側は、PayloadBinder オブジェクト(バイト配列)を配置できます。送信側が他のデータ型を送信する必要がある場合は、データをバイト配列にシリアル化し、バイト配列を使用して Payload オブジェクトを構築し、Payload を送信しなければなりません。次に、受信側クライアントは受信した Payload からバイト配列を取得し、想定されるデータ オブジェクトにバイト配列を逆シリアル化します。たとえば、送信側が ID FragmentB の受信側エンドポイントに文字列 hello を送信する場合は、プロトコル バッファを使用して次のようなデータ型を定義できます。

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 がクライアント アプリ内で一意である限り、複数の受信側エンドポイントを登録できます。receiverEndpointId は、AbstractReceiverService により、Payload のディスパッチ先の受信側エンドポイントを決定するために使用されます。次に例を示します。

  • 送信側は Payload 内で receiver_endpoint_id:FragmentB を指定します。Payload を受信すると、受信側の AbstractReceiverServiceforwardPayload("FragmentB", payload) を呼び出して Payload を FragmentB にディスパッチします。
  • 送信側は Payload 内で data_type:VOLUME_CONTROL を指定します。Payload を受信すると、受信側の AbstractReceiverService は、このタイプの PayloadFragmentB にディスパッチすべきであることを認識しているため、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

nulll の CarRemoteDeviceManager と CarOccupantConnectionManager

考えられる根本原因を調べるには:

  1. カーサービスがクラッシュしたとします。上記のように、カーサービスがクラッシュすると、2 つのマネージャーは意図的に null にリセットされます。カーサービスが再起動されると、2 つのマネージャーは 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]
      
    • デフォルトでは、これら 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 にペイロードを送信できるが、その逆はできない

設計上、接続は一方向のみ可能です。双方向接続を確立するには、client1client2 の両方が互いに接続をリクエストして、承認を得なければなりません。