Ứng dụng đặc quyền của hệ thống trong AAOS có thể sử dụng API giao tiếp nhiều màn hình để 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 ngồi 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 có người
Khái niệm vùng người ngồi liên kết người dùng với một nhóm màn hình. Mỗi vùng người ngồi có một màn hình với loại DISPLAY_TYPE_MAIN. Vùng người ngồi cũng có thể có các màn hình bổ sung, chẳng hạn như màn hình cụm đồng hồ. Mỗi vùng người ngồi đượ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 SoC đơn, tất cả các khu vực người ngồi 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:
API quản lý nguồn điện cho phép ứng dụng quản lý nguồn điện của màn hình trong khu vực người ngồi.
API Khám phá cho phép ứng dụng theo dõi trạng thái của các khu vực dành cho người ngồi khác trong ô tô và theo dõi các ứng dụng ngang hàng trong các khu vực dành cho người ngồi đó. Sử dụng API Khám phá trước khi sử dụng API Kết nối.
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 người ngồi khác và gửi tải trọng đến ứng dụng ngang hàng.
Bạn cần có API Khám phá và API Kết nối để kết nối. API quản lý nguồn điện là không bắt buộc.
Comms API không hỗ trợ việc giao tiếp giữa các ứng dụng. Thay vào đó, phương thức này 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 các người dùng hiển thị 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 với ứng dụng nhận này. Nếu cần xác nhận của người dùng để thiết lập kết nối, MyReceiverService
có thể ghi đè phương thức này để khởi 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ỉ gọi acceptConnection()
.`
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` có thể ghi đè phương thức này để:
- Chuyển tiếp
Payload
đến(các) điểm cuối của trình nhận tương ứng, nếu có. Để nhận các điểm cuối của trình nhận đã đăng ký, hãy gọigetAllReceiverEndpoints()
. Để chuyển tiếpPayload
đến một điểm cuối của trình nhận nhất định, hãy gọiforwardPayload()
HOẶC,
- Lưu
Payload
vào bộ nhớ đệm và gửi khi điểm cuối của trình nhận dự kiến được đăng ký, trong đóMyReceiverService
được thông báo thông quaonReceiverRegistered()
Khai báo AbstractReceiverService
Ứng dụng receiver 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 này 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 khách 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ố ba quyền ở trên đều là quyền đặc quyền, PHẢI được cấp trước bởi 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>
Tải trình quản lý Ứng dụng ô tô
Để sử dụng API, ứng dụng khách PHẢI đăng ký CarServiceLifecycleListener
để nhận 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 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 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 người ngồi trên thiết bị nhận và ứng dụng nhận đều được đặt. Nếu không, có thể xảy ra lỗi. 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 chỉ nên yêu cầu người gửi 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ố 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 để người dùng phê duyệt 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 người dùng tốt hơn, 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 yêu cầu 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ột 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ó thống nhất với 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 thị 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
, thì người gửi có thể hiển thị thông báo ngắn hoặc hộp thoại để nhắc người dùng mở khoá màn hình của khu vực người ngồi trong xe nhận.
Khi không cần tìm thiết bị nhận nữa (ví dụ: khi tìm thấy tất cả thiết bị nhận và thiết lập kết nối hoặc không hoạt động), thiết bị gửi CÓ THỂ dừng quá trình tìm thiết bị nhận.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Khi tính năng khám phá bị dừng, các kết nối hiện có sẽ không bị ảnh hưởng. Trình gửi có thể tiếp tục gửi Payload
đến các trình 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 trình nhận được đặt, trình gửi CÓ THỂ yêu cầu kết nối với trình 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 trình gửi yêu cầu kết nối với trình nhận, AbstractReceiverService
trong ứng dụng nhận sẽ được liên kết với dịch vụ ô tô và AbstractReceiverService.onConnectionInitiated()
sẽ được gọi. Như đã giải thích trong phần (Sender) Request Connection (Yêu cầu kết nối (Người gử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 thiết lập kết nối, người gửi CÓ THỂ gửi Payload
đến 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);
}
}
Bên gửi có thể đặt một đối tượng Binder
hoặc một mảng byte vào Payload
. Nếu trình gửi cần gửi các loại dữ liệu khác, thì trình gửi PHẢI chuyển đổi tuần tự dữ liệu thành một mảng byte, sử dụng mảng byte để tạo đố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à chuyển đổi tuần tự mảng byte thành đối tượng dữ liệu dự kiến.
Ví dụ: nếu trình gửi muốn gửi một Chuỗi hello
đến điểm cuối của trình nhận có mã nhận dạng FragmentB
, thì trình gửi có thể sử dụng Vùng đệm Proto để xác định 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ụ tiếp 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 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 trình nhận tương ứng hoặc lưu Payload
vào bộ nhớ đệm rồi gửi sau khi đăng ký điểm cuối của trình nhận dự kiến.
(Điểm cuối của trình nhận) Đăng ký và huỷ đăng ký
Ứng dụng receiver (trình nhận) PHẢI gọi registerReceiver()
để đăng ký các điểm cuối của trình nhận. Một trường hợp sử dụng thông thường là một Mảnh cần nhận Payload
, vì vậy, mảnh này sẽ đăng ký một điểm cuối của trình 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 trình 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 trình nhận miễn là receiverEndpointId
của chúng là duy nhất trong ứng dụng khách. receiverEndpointId
sẽ được AbstractReceiverService
sử dụng để quyết định(các) điểm cuối của trình nhận sẽ gửi Trọng tải đến. Ví dụ:
- Người gửi chỉ định
receiver_endpoint_id:FragmentB
trongPayload
. Khi nhận đượcPayload
,AbstractReceiverService
trong trình thu sẽ gọiforwardPayload("FragmentB", payload)
để gửi Trọng tải đếnFragmentB
- Người gửi chỉ định
data_type:VOLUME_CONTROL
trongPayload
. Khi nhận đượcPayload
,AbstractReceiverService
trong trình nhận biết rằng loạiPayload
này sẽ được gửi đếnFragmentB
, vì vậy, nó sẽ gọiforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Người gửi) Chấm dứt kết nối
Khi người gửi không cần gửi Payload
đến người nhận nữa (ví dụ: người gửi không hoạt động), người gửi PHẢI chấm dứt kết nối.
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.disconnect(receiverZone);
}
Sau khi bị ngắt kết nối, người gửi không thể gửi Payload
cho người 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 sau để 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"
Để 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
CarRemoteDeviceManager và CarOccupantConnectionManager rỗng
Hãy xem những nguyên nhân gốc rễ có thể xảy ra sau đây:
Dịch vụ xe gặp sự cố. Như đã minh hoạ trước đó, hai trình quản lý được đặt lại thành
null
khi dịch vụ xe gặp sự cố. Khi dịch vụ bảo dưỡng xe được khởi động lại, hai trình quản lý sẽ được đặt thành giá trị không rỗng.Bạn chưa bật
CarRemoteDeviceService
hoặcCarOccupantConnectionService
. Để xác định xem một hoặc một tính năng khác có được bật hay không, hãy chạy:adb shell dumpsys car_service --services CarFeatureController
Tìm
mDefaultEnabledFeaturesFromConfig
, chứacar_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 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 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 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 sai API:
registerStateCallback()
Ứng dụng khách này đã đăng ký mộtStateCallback
.unregisterStateCallback()
Thực thểCarRemoteDeviceManager
này chưa đăng kýStateCallback
nào.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ý để 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.
Ứng dụng 1 có thể gửi Tải trọng đến ứng dụng 2, nhưng không thể làm ngược lại
Theo thiết kế, kết nối này là 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 mới được phê duyệt.