다중 디스플레이 커뮤니케이션 API

AAOS의 시스템 권한 앱에서 Multi-Display Communications API를 사용하여 자동차의 다른 승객 공간에서 실행되는 동일한 앱 (동일한 패키지 이름)과 통신할 수 있습니다. 이 페이지에서는 API를 통합하는 방법을 설명합니다. 자세한 내용은 CarOccupantZoneManager.OccupantZoneInfo를 참고하세요.

점유자 영역

사용자 구역 개념은 사용자를 디스플레이 세트에 매핑합니다. 각 탑승자 구역에는 DISPLAY_TYPE_MAIN 유형의 디스플레이가 있습니다. 탑승자 영역에는 계기판 디스플레이와 같은 추가 디스플레이가 있을 수도 있습니다. 각 탑승자 영역에는 Android 사용자가 할당됩니다. 각 사용자는 자체 계정과 앱을 보유합니다.

하드웨어 구성

Comms API는 단일 SoC만 지원합니다. 단일 SoC 모델에서 모든 탑승자 영역과 사용자는 동일한 SoC에서 실행됩니다. Comms API는 다음 세 가지 구성요소로 구성됩니다.

  • 전원 관리 API를 사용하면 클라이언트가 탑승자 영역의 디스플레이 전원을 관리할 수 있습니다.

  • Discovery API를 사용하면 클라이언트가 차량의 다른 승객 공간의 상태를 모니터링하고 해당 승객 공간의 피어 클라이언트를 모니터링할 수 있습니다. Connection API를 사용하기 전에 Discovery API를 사용하세요.

  • Connection API를 사용하면 클라이언트가 다른 거주자 영역의 피어 클라이언트에 연결하고 피어 클라이언트에 페이로드를 전송할 수 있습니다.

연결하려면 Discovery API와 Connection API가 필요합니다. Power 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()를 호출할 있습니다. 그렇지 않으면 MyReceiverServiceacceptConnection()를 호출하면 됩니다.`

onPayloadReceived()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.MyReceiverService` 는 이 메서드를 재정의하여 다음을 실행할 있습니다.

  • 해당하는 수신자 엔드포인트(있는 경우)에 Payload를 전달합니다. 등록된 수신기 엔드포인트를 가져오려면 getAllReceiverEndpoints()를 호출합니다. Payload를 지정된 수신기 엔드포인트로 전달하려면 forwardPayload()를 호출합니다.

또는

  • Payload를 캐시하고 예상 수신자 엔드포인트가 등록되면 전송합니다. 이 엔드포인트는 onReceiverRegistered()를 통해 MyReceiverService에 알림을 받습니다.

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

위의 세 가지 권한은 모두 권한이 있는 권한이며 허용 목록 파일에서 미리 부여해야 합니다(MUST). 예를 들어 다음은 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를 등록하여 연결된 Car 관리자를 가져와야 합니다.

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는 피어 앱이 동일한 긴 버전 코드(FLAG_CLIENT_SAME_LONG_VERSION) 및 서명(FLAG_CLIENT_SAME_SIGNATURE)을 가질 수 있도록 동일한 Android 인스턴스의 여러 사용자만 지원합니다. 따라서 앱은 두 값이 일치하는지 확인할 필요가 없습니다.

더 나은 사용자 환경을 위해 플래그가 설정되지 않은 경우 발신자 클라이언트는 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()가 호출됩니다. (Sender) Request Connection에 설명된 대로 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);
    }
}

발신자는 Binder 객체 또는 바이트 배열을 Payload에 넣을 수 있습니다. 발신자가 다른 데이터 유형을 전송해야 하는 경우 데이터를 바이트 배열로 직렬화하고, 바이트 배열을 사용하여 Payload 객체를 생성하고, Payload를 전송해야 합니다. 그런 다음 수신기 클라이언트는 수신된 Payload에서 바이트 배열을 가져와 바이트 배열을 예상 데이터 객체로 역직렬화합니다. 예를 들어 발신자가 ID가 FragmentB인 수신기 엔드포인트로 문자열 hello를 전송하려는 경우 Proto Buffers를 사용하여 다음과 같이 데이터 유형을 정의할 수 있습니다.

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가 클라이언트 앱 간에 고유한 한 여러 수신기 엔드포인트를 등록할 수 있습니다. receiverEndpointIdAbstractReceiverService에서 페이로드를 전달할 수신기 엔드포인트를 결정하는 데 사용됩니다. 예를 들면 다음과 같습니다.

  • 발신자가 Payloadreceiver_endpoint_id:FragmentB를 지정합니다. Payload를 수신하면 수신기의 AbstractReceiverServiceforwardPayload("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. CarRemoteDeviceServiceCarOccupantConnectionService의 내부 상태를 덤프하려면 다음을 실행합니다.

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

null CarRemoteDeviceManager 및 CarOccupantConnectionManager

다음과 같은 근본 원인이 있을 수 있습니다.

  1. 자동차 서비스가 비정상 종료되었습니다. 앞에서 설명한 대로 두 관리자는 자동차 서비스가 비정상 종료될 때 의도적으로 null로 재설정됩니다. 자동차 서비스가 다시 시작되면 두 관리자가 null이 아닌 값으로 설정됩니다.

  2. CarRemoteDeviceService 또는 CarOccupantConnectionService가 사용 설정되어 있지 않습니다. 둘 중 하나가 사용 설정되어 있는지 확인하려면 다음을 실행합니다.

    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()CarRemoteDeviceManager 인스턴스에서 등록된 StateCallback가 없습니다.
  • registerReceiver() receiverEndpointId은 이미 등록되어 있습니다.
  • unregisterReceiver() receiverEndpointId이 등록되지 않았습니다.
  • requestConnection() 대기 중인 연결 또는 연결이 이미 있습니다.
  • cancelConnection() 취소할 대기 중인 연결이 없습니다.
  • sendPayload() 연결이 설정되지 않았습니다.
  • disconnect() 연결이 설정되지 않았습니다.

클라이언트 1은 클라이언트 2에 페이로드를 전송할 수 있지만 그 반대의 경우는 불가능합니다.

연결은 설계상 단방향입니다. 양방향 연결을 설정하려면 client1client2 모두 서로 연결을 요청한 후 승인을 받아야 합니다.