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()
를 호출할 수 있습니다. 그렇지 않으면 MyReceiverService
가 acceptConnection()
를 호출하면 됩니다.`
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom 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_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는 피어 앱이 동일한 긴 버전 코드(
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;
}
그림 1은 Payload
흐름을 보여줍니다.
(수신기 서비스) 페이로드 수신 및 전달
수신기 앱이 Payload
을 수신하면 AbstractReceiverService.onPayloadReceived()
이 호출됩니다. 페이로드 전송에 설명된 대로 onPayloadReceived()
는 추상화된 메서드이며 클라이언트 앱에서 구현해야 합니다. 이 메서드에서 클라이언트는 Payload
를 상응하는 수신기 엔드포인트로 전달하거나 Payload
를 캐시한 후 예상 수신기 엔드포인트가 등록되면 전송할 수 있습니다.
(수신기 엔드포인트) 등록 및 등록 취소
수신기 앱은 registerReceiver()
를 호출하여 수신기 엔드포인트를 등록해야 합니다. 일반적인 사용 사례는 Fragment가 Payload
를 수신해야 하므로 수신기 엔드포인트를 등록하는 경우입니다.
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
수신기 클라이언트의 AbstractReceiverService
가 Payload
를 수신기 엔드포인트로 전달하면 연결된 PayloadCallback
가 호출됩니다.
클라이언트 앱은 receiverEndpointId
가 클라이언트 앱 간에 고유한 한 여러 수신기 엔드포인트를 등록할 수 있습니다. 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
를 전송할 필요가 없으면 (예: 비활성 상태가 됨) 연결을 종료해야 합니다.
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
mDefaultEnabledFeaturesFromConfig
를 찾습니다. 여기에는car_remote_device_service
및car_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에 페이로드를 전송할 수 있지만 그 반대의 경우는 불가능합니다.
연결은 설계상 단방향입니다. 양방향 연결을 설정하려면 client1
와 client2
모두 서로 연결을 요청한 후 승인을 받아야 합니다.