Multi-Display Communications API

Multi-Display Communications API は、システム特権アプリでの使用が可能です。 AAOS が別のアプリ(同じパッケージ名)で実行されている同じアプリと通信 車内の乗員ゾーンですこのページでは、API を統合する方法について説明します。学習内容 さらに、 CarOccupantZoneManager.OccupantZoneInfo

乗員ゾーン

乗員ゾーンという概念では、ユーザーを一連のディスプレイにマッピングします。各 乗員ゾーンにはディスプレイが DISPLAY_TYPE_MAIN。 乗員ゾーンには、クラスタ ディスプレイなどの追加ディスプレイも含まれる場合があります。 各乗員ゾーンには Android ユーザーが割り当てられます。各ユーザーは自分のアカウントを持つ できます。

ハードウェア構成

Comms API がサポートする SoC は 1 つのみです。単一 SoC モデルでは、乗員はすべて ゾーンとユーザーは同じ SoC で実行されます。Comms API は、次の 3 つのコンポーネントで構成されています。

  • Power Management API: クライアントがデバイスの電力 エリアに表示されます。

  • Discovery API により、クライアントは他の乗員の状態を監視できる 乗員ゾーン内のピア クライアントを監視できます。使用 Discovery 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_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);

(送信者)Discover

送信側クライアントは、受信側クライアントに接続する前に、 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が追加要件となります。1 つの ユーザー エクスペリエンスの向上、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 を receive:

if (mOccupantConnectionManager != null) {
    Payload payload = ...;
    try {
        mOccupantConnectionManager.sendPayload(receiverZone, payload);
    } catch (CarOccupantConnectionManager.PayloadTransferException e) {
        Log.e(TAG, "Failed to send Payload to " + receiverZone);
    }
}

送信側は、Binder オブジェクトまたはバイト配列を Payload に配置できます。もし 送信側が他のデータ型を送信する必要がある場合は、データをバイト値にシリアル化しなければなりません。 そのバイト配列を使用して Payload オブジェクトを作成し、 Payload。次に、受信側のクライアントは受信したバイト配列から Payload とし、バイト配列を想定されるデータ オブジェクトにシリアル化解除します。 たとえば、送信者が文字列 hello を受信者に送信する場合などです。 そのエンドポイントの ID が FragmentB の場合、プロトコル バッファを使用して 例:

message MyData {
  required string receiver_endpoint_id = 1;
  required string data = 2;
}

図 1Payload フローを示します。

ペイロードを送信する

図 1. ペイロードを送信する。

(Receiver サービス)ペイロードを受信して送信する

受信側のアプリが 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 は次のようになります 呼び出すことができます。

クライアント アプリは、デバイスが 1 つである限り、複数のレシーバ エンドポイントを登録できます。 receiverEndpointId はクライアント アプリ間で一意です。receiverEndpointId AbstractReceiverService がどのレシーバーを使用するかを決定するために使用されます。 ペイロードを送信するエンドポイントを指定できます。例:

  • 送信者が Payloadreceiver_endpoint_id:FragmentB を指定している。日時 Payload を受け取ると、レシーバーの AbstractReceiverService が呼び出します。 forwardPayload("FragmentB", payload) を使用してペイロードを FragmentB
  • 送信者が Payloaddata_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. CarRemoteDeviceService の内部状態をダンプし、 CarOccupantConnectionService:

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
    

CarRemoteDeviceManager と CarOccupantConnectionManager が null である

考えられる根本原因は次のとおりです。

  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() StateCallback は登録されたものではありません CarRemoteDeviceManager インスタンス。
  • registerReceiver() receiverEndpointId はすでに登録されています。
  • unregisterReceiver() receiverEndpointId は登録されていません。
  • requestConnection() 保留中または確立済みの接続がすでに存在します。
  • cancelConnection() キャンセルできる保留中の接続はありません。
  • sendPayload(): 接続が確立されていません。
  • disconnect(): 接続が確立されていません。

Client1 は client2 にペイロードを送信できるが、その逆はできない

接続は一方向の設計です。双方向接続を確立するには、 client1client2 は相互に接続をリクエストしなければならず、 承認を得る必要があります。