Một ứng dụng đặc quyền của hệ thống có thể sử dụng API Truyền thông đa màn hình trong AAOS để giao tiếp với cùng một ứng dụng (cùng tên gói) chạy trong khu vực của người trên xe ô tô. Trang này mô tả cách tích hợp API này. Để tìm hiểu hơn, bạn cũng có thể xem CarOccupantZoneManager.OccupantZoneInfo.
Vùng có người
Khái niệm khu vực có người ở liên kết người dùng đến một nhóm màn hình. Một Khu vực của người cư trú có một màn hình cùng loại DISPLAY_TYPE_MAIN. Khu vực có người cũng có thể có thêm các màn hình khác, chẳng hạn như màn hình cụm đồng hồ. Mỗi khu vực cư trú được chỉ định một người dùng Android. Mỗi người dùng có tài khoản riêng của họ và ứng dụ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ả người cư trú và người dùng chạy trên cùng một SoC. Comms API bao gồm ba thành phần:
API quản lý nguồn cho phép ứng dụng quản lý sức mạnh của hiển thị trong khu vực có người ở.
API Khám phá cho phép ứng dụng theo dõi trạng thái của những người cư trú khác trong xe và để giám sát những khách hàng ngang hàng trong khu vực của những người ngồi đó. Sử dụng Discovery API 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 vùng khác của người cư trú và gửi tải trọng đến ứng dụng ngang hàng.
Bạn cần có API Discovery và API Kết nối để kết nối. Sức mạnh Management API là không bắt buộc.
Comms API không hỗ trợ hoạt động giao tiếp giữa các ứng dụng. Thay vào đó, ứng dụng này chỉ được thiết kế cho hoạt động giao tiếp giữa các ứng dụng có cùng tên gói và chỉ dùng để giao tiếp giữa những 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 của người 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
vào kết quả. Nếu không, MyReceiverService
có thể chỉ cần gọi
acceptConnection()
.
onPayloadReceived()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
MyReceiverService` có thể ghi đè tùy chọn này
để:
- Chuyển tiếp
Payload
đến(các) điểm cuối tương ứng của receiver, nếu có. Người nhận lấy các điểm cuối của receiver đã đăng ký, hãy gọigetAllReceiverEndpoints()
. Người nhận chuyển tiếpPayload
đến một điểm cuối của receiver cụ thể, hãy gọiforwardPayload()
HOẶC,
- Lưu
Payload
vào bộ nhớ đệm và gửi khi điểm cuối dự kiến của receiver là đã đăng ký màMyReceiverService
sẽ được thông báo quaonReceiverRegistered()
Khai báo AbstractReceiverService
Ứng dụng nhận PHẢI khai báo AbstractReceiverService
được triển khai trong
tệp kê khai, hãy thêm bộ lọc ý định bằng 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 này, nên một ứng dụng khác có thể liên kết với
và gửi trực tiếp Payload
tới 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 và PHẢI
được cấp trước bằng các tệp danh sách cho phép. Ví dụ: đâ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ý ô tô
Để sử dụng API này, ứng dụng khách PHẢI đăng ký CarServiceLifecycleListener
để
xem 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 của người nhận, ứng dụng của người gửi NÊN khám phá
ứ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 người nhận, người gửi PHẢI đảm bảo tất cả các cờ vùng nhận tín hiệu và ứng dụng nhận tín hiệ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 nên yêu cầu kết nối với người nhận chỉ khi tất cả cờ của trình thu nhận được đặt. Tuy nhiên, vẫn có những 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ự chấp thuận của người dùng kết nối,
FLAG_OCCUPANT_ZONE_POWER_ON
vàFLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
trở thành yêu cầu bổ sung. Đối với nâng cao trải nghiệm người dùng,FLAG_CLIENT_RUNNING
và Bạn cũng nên dùngFLAG_CLIENT_IN_FOREGROUND
, nếu không người dùng có thể ngạc nhiên.Hiện tại (Android 15) chưa triển khai
FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
. Ứng dụng khách có thể bỏ qua thông báo đó.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 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 có hai giá trị đồng nhất.
Để cải thiện trải nghiệm người dùng, ứng dụng của người gửi CÓ THỂ hiển thị giao diện người dùng nếu cờ không
thiết lập. Ví dụ: nếu bạn không đặt FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
thì người gửi
có thể hiện một thông báo ngắn hoặc hộp thoại để nhắc người dùng mở khoá màn hình
vùng nhận tín hiệu.
Khi người gửi không còn cần phải tìm người nhận (ví dụ: khi tìm tất cả các receiver và kết nối đã thiết lập hoặc trở nên không hoạt động), nó CAN dừng khám phá.
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
Khi tính năng khám phá bị ngừng, 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 receiver đã 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, người gửi CAN yêu cầu kết nối đến người 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ụ người 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 nhận sẽ bị dịch vụ ô tô liên kết,
và AbstractReceiverService.onConnectionInitiated()
sẽ được gọi. Như
đã giải thích trong (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ằng
ứng dụng khách.
Khi người nhận chấp nhận yêu cầu kết nối, thông tin
ConnectionRequestCallback.onConnected()
sẽ được gọi, sau đó kết nối
đượ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);
}
}
Người gửi có thể đặt một đối tượng Binder
hoặc một mảng byte vào Payload
. Nếu
người gửi cần gửi các loại dữ liệu khác, nó PHẢI chuyển đổi tuần tự dữ liệu thành một byte
mảng, sử dụng mảng byte để tạo đối tượng Payload
và gửi đối tượng
Payload
. Sau đó, ứng dụng nhận sẽ lấy mảng byte từ giá trị nhận được
Payload
và giải tuần tự mảng byte vào đối tượng dữ liệu dự kiến.
Ví dụ: nếu người gửi muốn gửi Chuỗi hello
đến người nhận
điểm cuối có ID FragmentB
, thì điểm cuối này 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ạ quy trình Payload
:
(Dịch vụ bộ nhận) Nhận và gửi tải trọng
Sau khi ứng dụng nhận nhận được Payload
,
AbstractReceiverService.onPayloadReceived()
sẽ được gọi. Như được giải thích trong
Gửi tải trọng, onPayloadReceived()
là một
phương thức tóm tắt và PHẢI được ứng dụng khách triển khai. Trong phương thức này,
ứng dụng CAN chuyển tiếp Payload
đến(các) điểm cuối của bộ nhận tương ứng, hoặc
lưu Payload
vào bộ nhớ đệm, rồi gửi sau khi điểm cuối dự kiến của receiver là
đã đăng ký.
(Điểm cuối của người nhận) Đăng ký và huỷ đăng ký
Ứng dụng nhận NÊN gọi registerReceiver()
để đăng ký dịch vụ nhận
điểm cuối. Một trường hợp sử dụng điển hình là Mảnh cần nhận Payload
, vì vậy
nó sẽ đăng ký một điểm cuối của receiver:
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
Sau khi AbstractReceiverService
trong ứng dụng của dịch vụ nhận gửi
Payload
đến điểm cuối của receiver, thì PayloadCallback
được liên kết sẽ là
đã 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
là duy nhất trong số các ứng dụng khách. receiverEndpointId
sẽ được AbstractReceiverService
dùng để quyết định receiver nào
(các) điểm cuối để điều phối Tải trọng đến. Ví dụ:
- Người gửi chỉ định
receiver_endpoint_id:FragmentB
trongPayload
. Thời gian nhậnPayload
,AbstractReceiverService
trong các lệnh gọi receiverforwardPayload("FragmentB", payload)
để gửi Tải trọng đếnFragmentB
- Người gửi chỉ định
data_type:VOLUME_CONTROL
trongPayload
. Thời gian khi nhậnPayload
,AbstractReceiverService
trong receiver sẽ biết loạiPayload
này sẽ được gửi đếnFragmentB
, nên phương thức này sẽ gọiforwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(Người gửi) Chấm dứt kết nối
Sau khi người gửi không cần gửi Payload
cho người nhận nữa (ví dụ:
nó sẽ bị vô hiệu hoá), thì nó 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
Quy trình 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"
Để 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
Giá trị rỗng CarRemoteDeviceManager và CarOccupantConnectionManager
Hãy xem các 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 người quản lý là chủ động đặt lại thành
null
khi dịch vụ ô tô gặp sự cố. Trường hợp dịch vụ ô tô được khởi động lại, hai trình quản lý được đặt thành các giá trị khác rỗng.CarRemoteDeviceService
hoặcCarOccupantConnectionService
thì không bật. Để xác định xem một trong hai tuỳ chọn đã được bật hay chưa, hãy chạy mã:adb shell dumpsys car_service --services CarFeatureController
Tìm
mDefaultEnabledFeaturesFromConfig
, trong đó có 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 có thiết bị hỗ trợ chế độ 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ụ 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>
Trường hợp 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 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ề việc sử dụng sai API:
registerStateCallback()
Khách hàng này đã đăng ký mộtStateCallback
.unregisterStateCallback()
Chưa cóStateCallback
nào được đăng ký bởi Thực thểCarRemoteDeviceManager
.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 đã được thiết lập.cancelConnection()
Không có kết nối nào đang chờ xử lý để huỷ.sendPayload()
Chưa thiết lập kết nối nào.disconnect()
Chưa thiết lập kết nối nào.
Client1 có thể gửi Tải trọng đến client2, nhưng không thể gửi theo cách khác
Kết nối có một chiều theo thiết kế. Để thiết lập kết nối hai chiều, cả hai
client1
và client2
PHẢI yêu cầu kết nối với nhau, sau đó
được phê duyệt.