Bạn có thể dùng Multi-Display Communications API (API Giao tiếp trên nhiều màn hình) trong AAOS để giao tiếp với cùng một ứng dụng (cùng tên gói) đang chạy ở khu vực của những người khác trong xe. Trang này mô tả cách tích hợp API. Để tìm hiểu thêm, bạn cũng có thể xem CarOccupantZoneManager.OccupantZoneInfo.
Khu vực của người dùng
Khái niệm về khu vực của người dùng liên kết một người dùng với một nhóm màn hình. Mỗi khu vực của người dùng có một màn hình thuộc loại DISPLAY_TYPE_MAIN. Khu vực của người dùng cũng có thể có các màn hình khác, chẳng hạn như màn hình cụm. Mỗi khu vực của người dùng được chỉ định một người dùng Android. Mỗi người dùng có tài khoản và ứng dụng riêng.
Cấu hình phần cứng
Comms API chỉ hỗ trợ một SoC. Trong mô hình một SoC, tất cả khu vực của người dùng và người dùng đều chạy trên cùng một SoC. Comms API bao gồm 3 thành phần:
Power management API (API Quản lý nguồn) cho phép ứng dụng quản lý nguồn của các màn hình trong khu vực của người dùng.
Discovery API (API Khám phá) cho phép ứng dụng theo dõi trạng thái của các khu vực khác trong xe và theo dõi các ứng dụng ngang hàng trong những khu vực đó. Hãy sử dụng Discovery API trước khi sử dụng Connection API.
Connection API (API Kết nối) cho phép ứng dụng kết nối với ứng dụng ngang hàng trong một khu vực khác của người dùng và gửi tải trọng đến ứng dụng ngang hàng đó.
Bạn cần có Discovery API và Connection API để kết nối. Power management API là không bắt buộc.
Comms API không hỗ trợ giao tiếp giữa các ứng dụng khác nhau. Thay vào đó, API này được thiết kế chỉ để giao tiếp giữa các ứng dụng có cùng tên gói và được dùng chỉ để giao tiếp giữa những người dùng khác nhau có thể nhìn thấy nhau.
Hướng dẫn tích hợp
Triển khai AbstractReceiverService
Để nhận Payload, ứng dụng nhận PHẢI triển khai các phương thức trừu tượng được xác định trong AbstractReceiverService. Ví dụ:
public class MyReceiverService extends AbstractReceiverService {
@Override
public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
}
@Override
public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
@NonNull Payload payload) {
}
}
onConnectionInitiated() được gọi khi ứng dụng gửi yêu cầu kết nối với ứng dụng nhận này. Nếu cần người dùng xác nhận để thiết lập kết nối, MyReceiverService có thể ghi đè phương thức này để chạy một hoạt động cấp quyền và gọi acceptConnection() hoặc rejectConnection() dựa trên kết quả. Nếu không, MyReceiverService có thể chỉ cần gọi acceptConnection().
onPayloadReceived() được gọi khi MyReceiverService nhận được
Payload từ ứng dụng gửi. MyReceiverService có thể ghi đè phương thức này để:
- Chuyển tiếp
Payloadđến (các) điểm cuối tương ứng của ứng dụng nhận (nếu có). Để nhận các điểm cuối đã đăng ký của ứng dụng nhận, hãy gọigetAllReceiverEndpoints(). Để chuyển tiếpPayloadđến một điểm cuối nhất định của ứng dụng nhận, hãy gọiforwardPayload()
HOẶC,
- Lưu
Payloadvào bộ nhớ đệm và gửi khi điểm cuối dự kiến của ứng dụng nhận được đăng ký, màMyReceiverServiceđược thông báo thông quaonReceiverRegistered()
Khai báo AbstractReceiverService
Ứng dụng nhận PHẢI khai báo AbstractReceiverService đã triển khai trong tệp kê khai, thêm bộ lọc ý định có hành động android.car.intent.action.RECEIVER_SERVICE cho dịch vụ này và yêu cầu quyền 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>
Quyền android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE đảm bảo rằng chỉ khung mới có thể liên kết với dịch vụ này. Nếu dịch vụ này không yêu cầu quyền, thì một ứng dụng khác có thể liên kết với dịch vụ này và gửi trực tiếp Payload đến dịch vụ đó.
Khai báo quyền
Ứng dụng PHẢI khai báo các quyền trong tệp kê khai.
<!-- 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"/>
Mỗi quyền trong số 3 quyền ở trên đều là quyền đặc biệt, PHẢI được cấp trước bằng các tệp danh sách cho phép. Ví dụ: sau đây là tệp danh sách cho phép của ứng dụng 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>
Nhận trình quản lý ô tô
Để sử dụng API, ứng dụng PHẢI đăng ký CarServiceLifecycleListener để nhận các trình quản lý ô tô được liên kết:
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);
(Ứng dụng gửi) Khám phá
Trước khi kết nối với ứng dụng nhận, ứng dụng gửi NÊN khám phá ứng dụng nhận bằng cách đăng ký 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);
}
Trước khi yêu cầu kết nối với ứng dụng nhận, ứng dụng gửi NÊN đảm bảo rằng tất cả cờ của khu vực của người dùng và ứng dụng nhận đều được đặt. Nếu không, lỗi có thể xảy ra. Ví dụ:
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;
}
Bạn nên yêu cầu ứng dụng gửi kết nối với ứng dụng nhận chỉ khi tất cả cờ của ứng dụng nhận được đặt. Tuy nhiên, vẫn có một số trường hợp ngoại lệ:
FLAG_OCCUPANT_ZONE_CONNECTION_READYvàFLAG_CLIENT_INSTALLEDlà các yêu cầu tối thiểu cần thiết để thiết lập kết nối.Nếu ứng dụng nhận cần hiển thị một giao diện người dùng để được người dùng phê duyệt kết nối, thì
FLAG_OCCUPANT_ZONE_POWER_ONvàFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDsẽ trở thành các yêu cầu bổ sung. Để mang lại trải nghiệm tốt hơn cho người dùng, bạn cũng nên dùngFLAG_CLIENT_RUNNINGvàFLAG_CLIENT_IN_FOREGROUND, nếu không người dùng có thể sẽ ngạc nhiên.Hiện tại (Android 15),
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKEDchưa được triển khai. Ứng dụng có thể bỏ qua cờ này.Hiện tại (Android 15), Comms API chỉ hỗ trợ nhiều người dùng trên cùng một thực thể Android để các ứng dụng ngang hàng có thể có cùng mã phiên bản dài (
FLAG_CLIENT_SAME_LONG_VERSION) và chữ ký (FLAG_CLIENT_SAME_SIGNATURE). Do đó, các ứng dụng không cần xác minh rằng 2 giá trị này giống nhau.
Để mang lại trải nghiệm tốt hơn cho người dùng, ứng dụng gửi CÓ THỂ hiển thị một giao diện người dùng nếu một cờ không được đặt. Ví dụ: nếu FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED không được đặt, thì ứng dụng gửi có thể hiển thị một thông báo nhanh hoặc một hộp thoại để nhắc người dùng mở khoá màn hình của khu vực của người dùng nhận.
Khi không còn cần khám phá các ứng dụng nhận nữa (ví dụ: khi tìm thấy tất cả ứng dụng nhận và thiết lập kết nối hoặc trở nên không hoạt động), ứng dụng gửi CÓ THỂ dừng quá trình khám phá.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Khi quá trình khám phá dừng lại, các kết nối hiện có sẽ không bị ảnh hưởng. Ứng dụng gửi có thể tiếp tục gửi Payload đến các ứng dụng nhận đã kết nối.
(Ứng dụng gửi) Yêu cầu kết nối
Khi tất cả cờ của ứng dụng nhận được đặt, ứng dụng gửi CÓ THỂ yêu cầu kết nối với ứng dụng nhận:
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);
}
(Dịch vụ nhận) Chấp nhận kết nối
Sau khi ứng dụng gửi yêu cầu kết nối với ứng dụng nhận, AbstractReceiverService trong ứng dụng nhận sẽ được dịch vụ ô tô liên kết,
và AbstractReceiverService.onConnectionInitiated() sẽ được gọi. Như
giải thích trong phần (Ứng dụng gửi) Yêu cầu kết nối,
onConnectionInitiated() là một phương thức trừu tượng và PHẢI được ứng dụng triển khai.
Khi ứng dụng nhận chấp nhận yêu cầu kết nối, ConnectionRequestCallback.onConnected() của ứng dụng gửi sẽ được gọi, sau đó kết nối sẽ được thiết lập.
(Ứng dụng gửi) Gửi tải trọng
Sau khi kết nối được thiết lập, ứng dụng gửi CÓ THỂ gửi Payload đến ứng dụng nhận:
if (mOccupantConnectionManager != null) {
Payload payload = ...;
try {
mOccupantConnectionManager.sendPayload(receiverZone, payload);
} catch (CarOccupantConnectionManager.PayloadTransferException e) {
Log.e(TAG, "Failed to send Payload to " + receiverZone);
}
}
Ứng dụng gửi có thể đặt một đối tượng Binder hoặc một mảng byte trong Payload. Nếu cần gửi các loại dữ liệu khác, ứng dụng gửi PHẢI tuần tự hoá dữ liệu thành một mảng byte, sử dụng mảng byte để tạo một đối tượng Payload và gửi Payload. Sau đó, ứng dụng nhận sẽ nhận mảng byte từ Payload đã nhận và giải tuần tự hoá mảng byte thành đối tượng dữ liệu dự kiến.
Ví dụ: nếu ứng dụng gửi muốn gửi một chuỗi hello đến điểm cuối của ứng dụng nhận có mã nhận dạng FragmentB, thì ứng dụng đó có thể sử dụng Proto Buffers để xác định một loại dữ liệu như sau:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
Hình 1 minh hoạ luồng Payload:
(Dịch vụ nhận) Nhận và gửi tải trọng
Sau khi ứng dụng nhận nhận được Payload, AbstractReceiverService.onPayloadReceived() của ứng dụng đó sẽ được gọi. Như giải thích trong
phần Gửi tải trọng, onPayloadReceived() là một
phương thức trừu tượng và PHẢI được ứng dụng triển khai. Trong phương thức này, ứng dụng
CÓ THỂ chuyển tiếp Payload đến(các) điểm cuối tương ứng của ứng dụng nhận hoặc
lưu Payload vào bộ nhớ đệm rồi gửi khi điểm cuối dự kiến của ứng dụng nhận được
đăng ký.
(Điểm cuối của ứng dụng nhận) Đăng ký và huỷ đăng ký
Ứng dụng nhận NÊN gọi registerReceiver() để đăng ký các điểm cuối của ứng dụng nhận. Một trường hợp sử dụng điển hình là một Đoạn mã cần nhận Payload, vì vậy, đoạn mã này sẽ đăng ký một điểm cuối của ứng dụng nhận:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Sau khi AbstractReceiverService trong ứng dụng nhận gửi
Payload đến điểm cuối của ứng dụng nhận, PayloadCallback được liên kết sẽ được
gọi.
Ứng dụng CÓ THỂ đăng ký nhiều điểm cuối của ứng dụng nhận miễn là receiverEndpointId của các điểm cuối đó là duy nhất trong ứng dụng. receiverEndpointId sẽ được AbstractReceiverService sử dụng để quyết định(các) điểm cuối của ứng dụng nhận mà Payload sẽ được gửi đến. Ví dụ:
- Ứng dụng gửi chỉ định
receiver_endpoint_id:FragmentBtrongPayload. Khi nhận đượcPayload,AbstractReceiverServicetrong ứng dụng nhận sẽ gọiforwardPayload("FragmentB", payload)để gửi Payload đếnFragmentB - Ứng dụng gửi chỉ định
data_type:VOLUME_CONTROLtrongPayload. Khi nhận đượcPayload,AbstractReceiverServicetrong ứng dụng nhận biết rằng loạiPayloadnày phải được gửi đếnFragmentB, vì vậy, ứng dụng đó sẽ gọiforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Ứng dụng gửi) Chấm dứt kết nối
Sau khi không còn cần gửi Payload đến ứng dụng nhận nữa (ví dụ: trở nên không hoạt động), ứng dụng gửi NÊN chấm dứt kết nối.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Sau khi ngắt kết nối, ứng dụng gửi không còn có thể gửi Payload đến ứng dụng nhận nữa.
Luồng kết nối
Luồng kết nối được minh hoạ trong Hình 2.
Khắc phục sự cố
Kiểm tra nhật ký
Cách kiểm tra nhật ký tương ứng:
Chạy lệnh này để ghi nhật ký:
adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"Cách kết xuất trạng thái nội bộ của
CarRemoteDeviceServicevàCarOccupantConnectionService:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
CarRemoteDeviceManager và CarOccupantConnectionManager có giá trị null
Hãy xem các nguyên nhân gốc có thể gây ra vấn đề này:
Dịch vụ ô tô gặp sự cố. Như minh hoạ trước đó, 2 trình quản lý này sẽ được đặt lại thành
nullkhi dịch vụ ô tô gặp sự cố. Khi dịch vụ ô tô khởi động lại, 2 trình quản lý này sẽ được đặt thành các giá trị không phải là null.CarRemoteDeviceServicehoặcCarOccupantConnectionServicekhông được bật. Để xác định xem một trong 2 dịch vụ này có được bật hay không, hãy chạy:adb shell dumpsys car_service --services CarFeatureControllerTìm
mDefaultEnabledFeaturesFromConfig, trong đó phải cócar_remote_device_servicevàcar_occupant_connection_service. Ví dụ: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]Theo mặc định, 2 dịch vụ này bị tắt. Khi một thiết bị hỗ trợ nhiều màn hình, bạn PHẢI phủ tệp cấu hình này. Bạn có thể bật 2 dịch vụ này trong một tệp cấu hình:
// 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>
Ngoại lệ khi gọi API
Nếu ứng dụng không sử dụng API như dự kiến, thì có thể xảy ra ngoại lệ. Trong trường hợp này, ứng dụng có thể kiểm tra thông báo trong ngoại lệ và ngăn xếp sự cố để giải quyết vấn đề. Sau đây là các ví dụ về việc sử dụng sai API:
registerStateCallback()Ứng dụng này đã đăng ký mộtStateCallback.unregisterStateCallback()Không cóStateCallbacknào được thực thểCarRemoteDeviceManagernày đăng ký.registerReceiver()receiverEndpointIdđã được đăng ký.unregisterReceiver()receiverEndpointIdchưa được đăng ký.requestConnection()Đã tồn tại một kết nối đang chờ xử lý hoặc đã thiết lập.cancelConnection()Không có kết nối đang chờ xử lý để huỷ.sendPayload()Không có kết nối đã thiết lập.disconnect()Không có kết nối đã thiết lập.
Ứng dụng 1 có thể gửi Payload đến ứng dụng 2, nhưng không thể gửi theo chiều ngược lại
Theo thiết kế, kết nối chỉ theo một chiều. Để thiết lập kết nối 2 chiều, cả client1 và client2 PHẢI yêu cầu kết nối với nhau rồi được phê duyệt.