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

Multi-Display Communications API는 AAOS가 다른 시스템에서 실행되는 동일한 앱 (동일한 패키지 이름)과 통신합니다. 자동차의 점유 영역입니다. 이 페이지에서는 API를 통합하는 방법을 설명합니다. 배우기 위해 자세한 내용은 CarOccupantZoneManager.OccupantZoneInfo입니다.

점유 영역

점유 영역 개념은 사용자를 일련의 디스플레이에 매핑합니다. 각 점유 영역에는 DISPLAY_TYPE_MAIN을 사용합니다. 점유 영역에는 계기판 디스플레이와 같은 추가 디스플레이가 있을 수도 있습니다. 각 점유 영역에는 Android 사용자가 할당됩니다. 사용자마다 고유한 계정 보유 사용할 수 있습니다.

하드웨어 구성

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

  • 전원 관리 API를 사용하면 점유 영역에 표시됩니다.

  • Discovery API: 클라이언트가 다른 승객의 상태를 모니터링할 수 있음 영역을 구현하고 이러한 점유 영역의 피어 클라이언트를 모니터링할 수 있습니다. 사용 Discovery API에서 검사한 후 연결 API를 사용합니다.

  • Connection API를 사용하면 클라이언트가 다음 위치에서 피어 클라이언트에 연결할 수 있습니다. 다른 점유 영역으로 옮긴 다음 페이로드를 피어 클라이언트에 전송합니다.

연결하려면 Discovery API 및 Connection API가 필요합니다. 더 파워 관리 API는 선택사항입니다.

Comms API는 서로 다른 앱 간의 통신을 지원하지 않습니다. 대신 패키지 이름이 같은 앱 간의 통신용으로만 설계되었습니다. 표시되는 여러 사용자 간의 커뮤니케이션에만 사용됩니다.

통합 가이드

AbstractReceiverService 구현

Payload를 수신하려면 수신기 앱이 추상 메서드를 구현해야 합니다(MUST). 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 a페이로드from the sender client.MyReceiverService` 는 이를 재정의할 수 있습니다. 사용하여 다음 작업을 수행합니다.

  • 상응하는 수신자 엔드포인트(있는 경우)로 Payload를 전달합니다. 받는사람 등록된 수신자 엔드포인트를 가져오려면 getAllReceiverEndpoints()를 호출합니다. 받는사람 Payload를 지정된 수신자 엔드포인트로 전달하고 forwardPayload()를 호출합니다.

또는

  • Payload를 캐시하고 예상되는 수신자 엔드포인트가 다음과 같을 때 전송합니다. 이를 통해 MyReceiverService는 <ph type="x-smartling-placeholder">onReceiverRegistered()</ph>

AbstractReceiverService 선언

수신기 앱은 구현된 AbstractReceiverService를 다음과 같이 선언해야 합니다(MUST). 매니페스트 파일, 작업이 있는 인텐트 필터 추가 이 서비스에 대해 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를 전송합니다.

권한 선언

클라이언트 앱은 매니페스트 파일에서 권한을 선언해야 합니다(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"/>

위의 세 가지 권한은 각각 독점 권한이며 허용 목록 파일에 의해 미리 부여됩니다. 예를 들어 다음은 앱 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는 추가 요구사항이 됩니다. 사용자 환경 개선, 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). 따라서 앱은 두 값이 일치합니다

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

발신자가 더 이상 수신자를 찾을 필요가 없는 경우 (예: 모든 수신기와 설정된 연결을 찾거나 비활성화되는 경우), 이는 CAN 발견을 중지합니다.

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()는 추상화된 메서드이며 다음을 통해 구현해야 합니다(MUST). 클라이언트 앱을 선택합니다.

수신자가 연결 요청을 수락하면, 발신자의 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 객체나 바이트 배열을 넣을 수 있습니다. 만약 발신자는 다른 데이터 유형을 전송해야 하는 경우 데이터를 바이트로 직렬화해야 합니다(MUST). 사용하고, 바이트 배열을 사용하여 Payload 객체를 생성하고, Payload 그런 다음 수신자 클라이언트는 수신된 Payload하고, 바이트 배열을 예상 데이터 객체로 역직렬화합니다. 예를 들어 발신자가 수신자에게 hello 문자열을 전송하려는 경우 엔드포인트가 ID가 FragmentB인 경우 Proto 버퍼를 사용하여 데이터 유형을 정의할 수 있음 :

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

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

페이로드 전송

그림 1. 페이로드를 보냅니다.

(수신자 서비스) 페이로드 수신 및 전달

수신기 앱이 Payload를 수신하면 AbstractReceiverService.onPayloadReceived()가 호출됩니다. 자세한 내용은 페이로드 전송에서 onPayloadReceived()는 추상화된 메서드이며 클라이언트 앱으로 구현해야 합니다(MUST). 이 메서드에서 클라이언트는 Payload를 상응하는 수신자 엔드포인트로 전달할 수 있습니다. Payload를 캐시한 다음 예상되는 수신자 엔드포인트가 있습니다.

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

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

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

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

수신자 클라이언트의 AbstractReceiverService가 다음을 전달하면 수신자 엔드포인트에 Payload로 전달하면 연결된 PayloadCallback는 다음과 같습니다. 호출됩니다.

클라이언트 앱이 수신 엔드포인트의 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를 수신자에게 전송할 필요가 없으면 (예: 비활성화됨) 연결을 종료해야 합니다(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
    
    • 다음을 포함하는 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]
      
    • 기본적으로 이 두 서비스는 사용 중지되어 있습니다. 기기에서 지원되는 경우 다중 디스플레이인 경우 이 구성 파일을 오버레이해야 합니다(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() 이 항목으로 등록된 StateCallback이(가) 없습니다. CarRemoteDeviceManager 인스턴스
  • registerReceiver() receiverEndpointId은(는) 이미 등록되어 있습니다.
  • unregisterReceiver() receiverEndpointId이(가) 등록되지 않았습니다.
  • requestConnection() 대기 중이거나 설정된 연결이 이미 있습니다.
  • cancelConnection() 취소할 대기 중인 연결이 없습니다.
  • sendPayload() 연결이 설정되지 않았습니다.
  • disconnect() 연결이 설정되지 않았습니다.

Client1이 client2에 페이로드를 전송할 수 있지만 그 반대의 경우는 허용되지 않습니다.

연결은 단방향으로 설계되었습니다. 양방향 연결을 설정하려면 client1client2는 서로 연결을 요청해야 합니다(MUST). 이후 승인을 받을 수 있습니다.