Một ứng dụng có đặc quyền hệ thống trong AAOS có thể dùng Multi-Display Communications API để giao tiếp với cùng một ứng dụng (cùng tên gói) đang chạy ở một khu vực khác của người dùng trong ô tô. 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.
Vùng người ngồi trong xe
Khái niệm về vùng người dùng sẽ liên kết một người dùng với một nhóm màn hình. Mỗi vùng cư trú đều có một màn hình thuộc loại DISPLAY_TYPE_MAIN. Khu vực dành cho người ngồi trong xe cũng có thể có các màn hình khác, chẳng hạn như màn hình phân cụm. Mỗi vùng dành cho người ngồi trong xe được chỉ định một người dùng Android. Mỗi người dùng đều 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 duy nhất. Trong mô hình SoC duy nhất, tất cả các vùng và người dùng của người cư ngụ đều chạy trên cùng một SoC. Comms API bao gồm 3 thành phần:
API quản lý nguồn cho phép ứng dụng quản lý nguồn của màn hình trong các vùng dành cho người ngồi.
Discovery API 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 đó. Sử dụng Discovery API trước khi sử dụng Connection API.
Connection API cho phép ứng dụng kết nối với ứng dụng ngang hàng trong một vùng khác 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. API Quản lý nguồn là không bắt buộc.
Comms API không hỗ trợ giao tiếp giữa các ứng dụng. Thay vào đó, nó chỉ được thiết kế để giao tiếp giữa các ứng dụng có cùng tên gói và chỉ được dùng để giao tiếp giữa những người dùng khác 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 đến ứng dụng nhận này. Nếu cần có sự xác nhận của người dùng để thiết lập kết nối, thì 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 của bộ nhận tương ứng (nếu có). Để nhận các điểm cuối của receiver đã đăng ký, hãy gọigetAllReceiverEndpoints()
. Để chuyển tiếpPayload
đến một điểm cuối nhận nhất định, hãy gọiforwardPayload()
HOẶC,
- Lưu vào bộ nhớ đệm
Payload
và gửi khi điểm cuối của thiết bị nhận dự kiến được đăng ký, màMyReceiverService
sẽ nhận đượ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ó thao tác 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 một Payload
đến dịch vụ.
Khai báo quyền
Ứng dụng khách PHẢI khai báo các quyền trong tệp kê khai của ứng dụng.
<!-- 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 và 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>
Mời người quản lý xe
Để sử dụng API này, ứng dụng khách PHẢI đăng ký một 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ười gửi) Khám phá
Trước khi kết nối với ứng dụng nhận, ứng dụng gửi PHẢI phát hiện ứng dụng nhận bằng cách đăng ký một 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 thiết bị nhận, thiết bị gửi PHẢI đảm bảo rằng tất cả cờ của vùng cư trú của thiết bị nhận và ứng dụng của thiết bị 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;
}
Người gửi chỉ nên yêu cầu kết nối với người nhận khi tất cả các cờ của người nhận đều được đặt. Tuy nhiên, vẫn có một số trường hợp ngoại lệ:
FLAG_OCCUPANT_ZONE_CONNECTION_READY
vàFLAG_CLIENT_INSTALLED
là 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ị giao diện người dùng để nhận được sự phê duyệt của người dùng về kết nối, thì
FLAG_OCCUPANT_ZONE_POWER_ON
vàFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
sẽ 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 sử dụngFLAG_CLIENT_RUNNING
và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_UNLOCKED
chưa được triển khai. Ứng dụng khách có thể bỏ qua thông báo 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 phiên bản 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 hai giá trị này có giống nhau hay không.
Để 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 giao diện người dùng nếu không đặt cờ. Ví dụ: nếu bạn không đặt FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
, người gửi có thể hiện một thông báo hoặc hộp thoại để nhắc người dùng mở khoá màn hình của vùng cư trú của người nhận.
Khi không cần tìm thấy các thiết bị nhận nữa (ví dụ: khi tìm thấy tất cả các thiết bị nhận và thiết lập kết nối hoặc trở nên không hoạt động), thiết bị gửi CÓ THỂ dừng quá trình tìm kiếm.
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ười gửi có thể tiếp tục gửi Payload
đến các thiết bị nhận đã kết nối.
(Người gửi) Yêu cầu kết nối
Khi tất cả cờ của thiết bị nhận được đặt, thiết bị gửi CÓ THỂ yêu cầu kết nối với thiết bị 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ười gửi yêu cầu kết nối với người nhận, AbstractReceiverService
trong ứng dụng người nhận sẽ được liên kết bằng dịch vụ trên ô tô và AbstractReceiverService.onConnectionInitiated()
sẽ được gọi. Như đã giải thích trong phần (Người 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 triển khai bởi ứng dụng khách.
Khi người nhận chấp nhận yêu cầu kết nối, ConnectionRequestCallback.onConnected()
của người gửi sẽ được gọi, sau đó kết nối sẽ được thiết lập.
(Người gửi) Gửi tải trọng
Sau khi kết nối được thiết lập, người gửi CÓ THỂ gửi Payload
cho người 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ười 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ười gửi PHẢI chuyển đổi dữ liệu thành một mảng byte, 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 được mảng byte từ Payload
đã nhận và giải tuần tự mảng byte thành đối tượng dữ liệu dự kiến.
Ví dụ: nếu người gửi muốn gửi một chuỗi hello
đến điểm cuối của người nhận có mã nhận dạng FragmentB
, thì người gửi có thể dùng Proto Buffers để xác định một kiểu dữ liệu như sau:
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
Hình 1 minh hoạ quy trình Payload
:
(Dịch vụ nhận) Nhận và gửi tải trọng
Sau khi nhận được Payload
, ứng dụng nhận sẽ gọi AbstractReceiverService.onPayloadReceived()
. 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 khách triển khai. Trong phương thức này, ứng dụng khách CÓ THỂ chuyển tiếp Payload
đến (các) điểm cuối của bộ nhận tương ứng hoặc lưu vào bộ nhớ đệm Payload
rồi gửi đi sau khi (các) điểm cuối của bộ nhận dự kiến được đăng ký.
(Điểm cuối của đầu thu) Đăng ký và huỷ đăng ký
Ứng dụng nhận PHẢI 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à Fragment cần nhận Payload
, vì vậy, Fragment sẽ đăng ký một điểm cuối của dịch vụ nhận:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Sau khi AbstractReceiverService
trong ứng dụng khách 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 khách CÓ THỂ đăng ký nhiều điểm cuối của bộ nhận, miễn là receiverEndpointId
của chúng là duy nhất trong ứng dụng khách. receiverEndpointId
sẽ được AbstractReceiverService
dùng để quyết định(các) điểm cuối của bộ nhận nào sẽ gửi Tải trọng đến. Ví dụ:
- Người gửi chỉ định
receiver_endpoint_id:FragmentB
trongPayload
. Khi nhậnPayload
,AbstractReceiverService
trong receiver sẽ gọiforwardPayload("FragmentB", payload)
để gửi Payload đếnFragmentB
- Người gửi chỉ định
data_type:VOLUME_CONTROL
trongPayload
. Khi nhận đượcPayload
,AbstractReceiverService
trong bộ nhận sẽ biết rằng loạiPayload
này phải được gửi đếnFragmentB
, vì vậy, nó sẽ gọiforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Người gửi) Ngắt kết nối
Khi người gửi không cần gửi Payload
cho người nhận nữa (ví dụ: khi người nhận không hoạt động), người gửi NÊN chấm dứt kết nối.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Sau khi bạn huỷ kết nối, người gửi sẽ không thể gửi Payload
cho người nhận nữa.
Quy trình 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
CarRemoteDeviceService
vàCarOccupantConnectionService
:adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager và CarOccupantConnectionManager
Hãy xem xét những nguyên nhân gốc rễ có thể gây ra vấn đề này:
Dịch vụ trên ô tô gặp sự cố. Như minh hoạ trước đó, hai trình quản lý này được đặt lại thành
null
một cách có chủ ý khi dịch vụ trên ô tô gặp sự cố. Khi dịch vụ ô tô khởi động lại, hai trình quản lý này sẽ được đặt thành giá trị không rỗng.CarRemoteDeviceService
hoặcCarOccupantConnectionService
không được bật. Để xác định xem một trong hai có được bật hay không, hãy chạy:adb shell dumpsys car_service --services CarFeatureController
Tìm
mDefaultEnabledFeaturesFromConfig
, trong đó phải cócar_remote_device_service
và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, hai dịch vụ này sẽ 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 hai 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ách không sử dụng API như dự kiến, thì có thể xảy ra một trường hợp ngoại lệ. Trong trường hợp này, ứng dụng khách có thể kiểm tra thông báo trong trường hợp ngoại lệ và ngăn xếp sự cố để giải quyết vấn đề. Sau đây là một số ví dụ về hành vi sử dụng API sai cách:
registerStateCallback()
Ứng dụng khách này đã đăng ký mộtStateCallback
.unregisterStateCallback()
Không cóStateCallback
nào được đăng ký bởi phiên bảnCarRemoteDeviceManager
này.registerReceiver()
receiverEndpointId
đã được đăng ký.unregisterReceiver()
receiverEndpointId
chưa được đăng ký.requestConnection()
Đã có 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ý nào để huỷ.sendPayload()
Không có kết nối nào được thiết lập.disconnect()
Không có kết nối nào được thiết lập.
Client1 có thể gửi Payload đến client2, nhưng không thể gửi theo chiều ngược lại
Theo thiết kế, kết nối này chỉ có một chiều. Để thiết lập kết nối hai chiều, cả client1
và client2
ĐỀU PHẢI yêu cầu kết nối với nhau rồi nhận được sự phê duyệt.