Multi-Display Communications API

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

점유 영역

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

하드웨어 구성

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

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

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

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

연결에는 Discovery API와 Connection API가 필요합니다. 전원 관리 API는 선택사항입니다.

Comms API는 서로 다른 앱 간의 통신을 지원하지 않습니다. 대신 동일한 패키지 이름의 앱 간 통신용으로 설계되었으며 서로 다른 표시 사용자 간 통신용으로 사용됩니다.

통합 가이드

AbstractReceiverService 구현

Payload를 수신하려면 수신자 앱이 AbstractReceiverService에 정의된 추상 메서드를 구현해야 합니다(MUST). 예를 들면 다음과 같습니다.

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()MyReceiverService이 발신자 클라이언트로부터 Payload를 수신한 경우 호출됩니다. MyReceiverService는 이 메서드를 재정의하여 다음 작업을 실행할 수 있습니다.

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

또는

  • Payload를 캐시하고 예상 수신자 엔드포인트가 등록될 때 전송합니다. 이 경우 MyReceiverServiceonReceiverRegistered()를 통해 알림을 받습니다.

AbstractReceiverService 선언

수신기 앱은 구현된 AbstractReceiverService를 매니페스트 파일에 선언하고, 이 서비스에 android.car.intent.action.RECEIVER_SERVICE 작업이 있는 인텐트 필터를 추가하고, android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE 권한을 요구해야 합니다(MUST).

<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를 직접 전송할 수 있습니다.

권한 선언

클라이언트 앱은 매니페스트 파일에서 권한을 선언해야 합니다(MUST).

<!-- 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를 등록하여 연결된 자동차 관리자를 가져와야 합니다(MUST).

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를 등록하여 수신자 클라이언트를 검색해야 합니다(SHOULD).

// 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);
}

수신기에 연결을 요청하기 전에 발신자는 수신기 점유자 영역과 수신기 앱의 모든 플래그가 설정되어 있는지 확인해야 합니다(SHOULD). 그렇지 않으면 오류가 발생할 수 있습니다. 예를 들면 다음과 같습니다.

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)을 가질 수 있습니다. 따라서 앱은 두 값이 일치하는지 확인할 필요가 없습니다.

사용자 환경을 개선하기 위해 플래그가 설정되지 않은 경우 발신자 클라이언트는 UI를 표시할 수 있습니다(CAN). 예를 들어 FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED이 설정되지 않은 경우 발신자는 토스트나 대화상자를 표시하여 사용자에게 수신자 점유자 영역의 화면을 잠금 해제하라고 메시지를 표시할 수 있습니다.

발신자가 더 이상 수신자를 검색할 필요가 없는 경우(예: 모든 수신자를 찾아 연결을 설정했거나 비활성화된 경우) 검색을 중지할 수 있습니다(CAN).

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()가 호출됩니다. (발신자) 연결 요청에 설명된 대로 onConnectionInitiated()는 추상화된 메서드이며 클라이언트 앱에서 구현해야 합니다(MUST).

수신자가 연결 요청을 수락하면 발신자의 ConnectionRequestCallback.onConnected()가 호출되고 연결이 설정됩니다.

(발신자) 페이로드 전송

연결이 설정되면 발신자는 수신자에게 Payload를 전송할 수 있습니다(CAN).

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를 전송해야 합니다(MUST). 그런 다음 수신자 클라이언트는 수신된 Payload에서 바이트 배열을 가져오고 바이트 배열을 예상 데이터 객체로 역직렬화합니다. 예를 들어 발신자가 ID가 FragmentB인 수신자 엔드포인트에 문자열 hello를 전송하려는 경우 프로토 버퍼를 사용하여 다음과 같은 데이터 유형을 정의할 수 있습니다.

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

그림 1Payload 흐름을 보여줍니다.

페이로드 전송

그림 1. 페이로드를 전송합니다.

(수신기 서비스) 페이로드 수신 및 디스패치

수신자 앱이 Payload을 수신하면 AbstractReceiverService.onPayloadReceived()이 호출됩니다. 페이로드 전송에 설명된 대로 onPayloadReceived()는 추상화된 메서드이며 클라이언트 앱에서 구현해야 합니다(MUST). 이 메서드에서 클라이언트는 Payload를 해당 수신자 엔드포인트로 전달하거나 Payload를 캐시한 후 예상 수신자 엔드포인트가 등록되면 전송할 수 있습니다(CAN).

(수신기 엔드포인트) 등록 및 등록 취소

수신기 앱은 registerReceiver()를 호출하여 수신기 엔드포인트를 등록해야 합니다(SHOULD). 일반적인 사용 사례는 프래그먼트가 Payload를 수신해야 하므로 수신자 엔드포인트를 등록하는 것입니다.

private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
    
};

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.registerReceiver("FragmentB",
                getActivity().getMainExecutor(), mPayloadCallback);
}

수신자 클라이언트의 AbstractReceiverServicePayload를 수신자 엔드포인트에 디스패치하면 연결된 PayloadCallback가 호출됩니다.

클라이언트 앱의 receiverEndpointId가 클라이언트 앱 간에 고유한 한 클라이언트 앱은 여러 수신자 엔드포인트를 등록할 수 있습니다(CAN). 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을 전송할 필요가 없으면(예: 비활성화됨) 연결을 종료해야 합니다(SHOULD).

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
    • car_remote_device_servicecar_occupant_connection_service가 포함되어야 하는 mDefaultEnabledFeaturesFromConfig를 찾습니다. 예를 들면 다음과 같습니다.

      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]
      
    • 기본적으로 이 두 서비스는 사용 중지되어 있습니다. 기기가 다중 디스플레이를 지원하는 경우 이 구성 파일을 오버레이해야 합니다(MUST). 구성 파일에서 두 서비스를 사용 설정할 수 있습니다.

      // 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 모두 서로에 대한 연결을 요청한 후 승인을 받아야 합니다(MUST).