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 when
MyReceiverServicehas 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_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를 표시할 수 있습니다.
설정합니다. 예를 들어 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);
}
}
발신자는 Payload
에 Binder
객체나 바이트 배열을 넣을 수 있습니다. 만약
발신자는 다른 데이터 유형을 전송해야 하는 경우 데이터를 바이트로 직렬화해야 합니다(MUST).
사용하고, 바이트 배열을 사용하여 Payload
객체를 생성하고,
Payload
그런 다음 수신자 클라이언트는 수신된
Payload
하고, 바이트 배열을 예상 데이터 객체로 역직렬화합니다.
예를 들어 발신자가 수신자에게 hello
문자열을 전송하려는 경우
엔드포인트가 ID가 FragmentB
인 경우 Proto 버퍼를 사용하여 데이터 유형을 정의할 수 있음
:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
그림 1은 Payload
흐름을 보여줍니다.
(수신자 서비스) 페이로드 수신 및 전달
수신기 앱이 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
에서 사용할 수신자를 결정합니다.
엔드포인트가 있어야 합니다. 예를 들면 다음과 같습니다.
- 발신자가
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
다음을 포함하는
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]
기본적으로 이 두 서비스는 사용 중지되어 있습니다. 기기에서 지원되는 경우 다중 디스플레이인 경우 이 구성 파일을 오버레이해야 합니다(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에 페이로드를 전송할 수 있지만 그 반대의 경우는 허용되지 않습니다.
연결은 단방향으로 설계되었습니다. 양방향 연결을 설정하려면
client1
및 client2
는 서로 연결을 요청해야 합니다(MUST). 이후
승인을 받을 수 있습니다.