다중 디스플레이 통신 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()
를 호출할 수 있습니다. 그렇지 않으면 MyReceiverService
는 acceptConnection()
를 호출할 수 있습니다.
onPayloadReceived()
는 MyReceiverService
이 발신자 클라이언트로부터 Payload
를 수신한 경우 호출됩니다. MyReceiverService
는 이 메서드를 재정의하여 다음 작업을 실행할 수 있습니다.
Payload
를 해당하는 수신자 엔드포인트로 전달합니다(있는 경우). 등록된 수신기 엔드포인트를 가져오려면getAllReceiverEndpoints()
를 호출합니다.Payload
를 지정된 수신자 엔드포인트로 전달하려면forwardPayload()
를 호출하세요.
또는
Payload
를 캐시하고 예상 수신자 엔드포인트가 등록될 때 전송합니다. 이 경우MyReceiverService
는onReceiverRegistered()
를 통해 알림을 받습니다.
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_READY
및FLAG_CLIENT_INSTALLED
는 연결을 설정하는 데 필요한 최소 요구사항입니다.수신자 앱이 연결에 대한 사용자 승인을 받기 위해 UI를 표시해야 하는 경우
FLAG_OCCUPANT_ZONE_POWER_ON
및FLAG_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를 표시할 수 있습니다(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;
}
그림 1은 Payload
흐름을 보여줍니다.
(수신기 서비스) 페이로드 수신 및 디스패치
수신자 앱이 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);
}
수신자 클라이언트의 AbstractReceiverService
가 Payload
를 수신자 엔드포인트에 디스패치하면 연결된 PayloadCallback
가 호출됩니다.
클라이언트 앱의 receiverEndpointId
가 클라이언트 앱 간에 고유한 한 클라이언트 앱은 여러 수신자 엔드포인트를 등록할 수 있습니다(CAN). receiverEndpointId
는 AbstractReceiverService
가 페이로드를 디스패치할 수신자 엔드포인트를 결정하는 데 사용됩니다. 예를 들면 다음과 같습니다.
- 발신자는
Payload
에receiver_endpoint_id:FragmentB
를 지정합니다.Payload
를 수신하면 수신기의AbstractReceiverService
이forwardPayload("FragmentB", payload)
를 호출하여 페이로드를FragmentB
에 디스패치합니다. - 발신자는
Payload
에data_type:VOLUME_CONTROL
를 지정합니다.Payload
를 수신할 때 수신기의AbstractReceiverService
은 이 유형의Payload
가FragmentB
에 디스패치되어야 한다는 것을 알고 있으므로forwardPayload("FragmentB", payload)
를 호출합니다.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(발신자) 연결 종료
발신자가 더 이상 수신자에게 Payload
을 전송할 필요가 없으면(예: 비활성화됨) 연결을 종료해야 합니다(SHOULD).
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
연결이 해제되면 발신자는 더 이상 수신자에게 Payload
를 보낼 수 없습니다.
연결 흐름
연결 흐름은 그림 2에 나와 있습니다.
문제 해결
로그 확인
해당 로그를 확인하려면 다음 단계를 따르세요.
로깅을 위해 다음 명령어를 실행합니다.
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
CarRemoteDeviceService
및CarOccupantConnectionService
의 내부 상태를 덤프하려면 다음을 실행하세요.adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
null CarRemoteDeviceManager 및 CarOccupantConnectionManager
가능한 근본 원인을 확인하세요.
자동차 서비스가 비정상 종료되었습니다. 앞서 설명한 것처럼 자동차 서비스가 비정상 종료되면 두 관리자는 의도적으로
null
로 재설정됩니다. 자동차 서비스가 다시 시작되면 두 관리자가 null이 아닌 값으로 설정됩니다.CarRemoteDeviceService
또는CarOccupantConnectionService
이 사용 설정되지 않았습니다. 둘 중 하나가 사용 설정되어 있는지 확인하려면 다음을 실행합니다.adb shell dumpsys car_service --services CarFeatureController
car_remote_device_service
및car_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에 페이로드를 전송할 수 있지만 그 반대는 불가능합니다.
연결은 설계상 단방향입니다. 양방향 연결을 설정하려면 client1
와 client2
모두 서로에 대한 연결을 요청한 후 승인을 받아야 합니다(MUST).