แอปที่มีสิทธิ์ของระบบใน AAOS สามารถใช้ Multi-Display Communications API เพื่อสื่อสารกับแอปเดียวกัน (ชื่อแพ็กเกจเดียวกัน) ที่ทำงานในโซนที่มีการเข้าใช้ที่แตกต่างกันในรถ หน้านี้จะอธิบายวิธีผสานรวม API ดูข้อมูลเพิ่มเติมได้ที่CarOccupantZoneManager.OccupantZoneInfo
โซนผู้โดยสาร
แนวคิดของโซนผู้ใช้จะเชื่อมโยงผู้ใช้กับชุดจอแสดงผล แต่ละโซน
ของผู้เข้าพักมีจอแสดงผลประเภท
DISPLAY_TYPE_MAIN
โซนผู้โดยสารอาจมีจอแสดงผลเพิ่มเติมด้วย เช่น จอแสดงผลแผงหน้าปัด
ระบบจะกำหนดผู้ใช้ Android ให้กับโซนผู้เข้าพักแต่ละโซน ผู้ใช้แต่ละรายจะมีบัญชีและแอปของตนเอง
การกำหนดค่าฮาร์ดแวร์
Comms API รองรับ SoC เพียงตัวเดียว ในโมเดล SoC เดียว โซนและผู้ใช้ทั้งหมดจะทำงานใน SoC เดียวกัน Comms API ประกอบด้วยคอมโพเนนต์ 3 อย่าง ได้แก่
API การจัดการพลังงานช่วยให้ไคลเอ็นต์จัดการพลังงานของ จอแสดงผลในโซนผู้เข้าพักได้
Discovery API ช่วยให้ไคลเอ็นต์ตรวจสอบสถานะของโซนที่มีการเข้าใช้อื่นๆ ในรถ และตรวจสอบไคลเอ็นต์เทียบเท่าในโซนที่มีการเข้าใช้เหล่านั้นได้ ใช้ Discovery API ก่อนใช้ Connection API
Connection API ช่วยให้ไคลเอ็นต์เชื่อมต่อกับไคลเอ็นต์เพียร์ใน โซนผู้เข้าพักอื่นและส่งเพย์โหลดไปยังไคลเอ็นต์เพียร์ได้
ต้องใช้ Discovery API และ Connection API สำหรับการเชื่อมต่อ Power management API เป็นตัวเลือก
Comms API ไม่รองรับการสื่อสารระหว่างแอปต่างๆ แต่ได้รับการออกแบบเฉพาะสำหรับการสื่อสารระหว่างแอปที่มีชื่อแพ็กเกจเดียวกัน และใช้เฉพาะสำหรับการสื่อสารระหว่างผู้ใช้ที่มองเห็นได้แตกต่างกัน
คู่มือการผสานรวม
ใช้ AbstractReceiverService
หากต้องการรับ Payload แอปตัวรับสัญญาณต้องใช้เมธอดแบบนามธรรม
ที่กำหนดไว้ใน 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() จะเรียกใช้เมื่อ MyReceiverService ได้รับ Payload จากไคลเอ็นต์ของผู้ส่ง MyReceiverService สามารถลบล้างเมธอดนี้เพื่อทำสิ่งต่อไปนี้
- ส่งต่อ
Payloadไปยังปลายทางผู้รับที่เกี่ยวข้อง(หากมี) หากต้องการ รับปลายทางตัวรับที่ลงทะเบียนไว้ ให้โทรหาgetAllReceiverEndpoints()หากต้องการ ส่งต่อPayloadไปยังปลายทางผู้รับที่ระบุ ให้เรียกใช้forwardPayload()
หรือ
- แคช
Payloadและส่งเมื่อลงทะเบียนปลายทางผู้รับที่คาดไว้ ซึ่งระบบจะแจ้งให้MyReceiverServiceทราบผ่านonReceiverRegistered()
ประกาศ AbstractReceiverService
แอปตัวรับสัญญาณต้องประกาศ AbstractReceiverService ที่ใช้งานใน
ไฟล์ Manifest เพิ่มตัวกรอง Intent ที่มีการดำเนินการ
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 ไปยังบริการนี้ได้โดยตรง
ประกาศสิทธิ์
แอปไคลเอ็นต์ต้องประกาศสิทธิ์ในไฟล์ Manifest
<!-- 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"/>
สิทธิ์ทั้ง 3 รายการข้างต้นเป็นสิทธิ์ที่มีสิทธิ์พิเศษ ซึ่งต้อง
ได้รับอนุญาตล่วงหน้าจากไฟล์รายการที่อนุญาต ตัวอย่างเช่น นี่คือไฟล์รายการที่อนุญาตของแอป 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);
(ผู้ส่ง) สำรวจ
ก่อนเชื่อมต่อกับไคลเอ็นต์ตัวรับ ไคลเอ็นต์ผู้ส่งควรค้นหาไคลเอ็นต์ตัวรับโดยการลงทะเบียน 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) เดียวกันได้ ด้วยเหตุนี้ แอปจึงไม่จำเป็นต้องยืนยันว่าค่าทั้ง 2 ค่าตรงกัน
ไคลเอ็นต์ของผู้ส่งแสดง UI ได้หากไม่ได้ตั้งค่าแฟล็ก เพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดียิ่งขึ้น เช่น หากไม่ได้ตั้งค่า FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED ผู้ส่ง
จะแสดงข้อความป๊อปอัปหรือกล่องโต้ตอบเพื่อแจ้งให้ผู้ใช้ปลดล็อกหน้าจอของ
โซนผู้รับ
เมื่อผู้ส่งไม่จำเป็นต้องค้นหาผู้รับอีกต่อไป (เช่น เมื่อค้นหาผู้รับทั้งหมดและสร้างการเชื่อมต่อหรือไม่มีการใช้งาน) ผู้ส่งจะหยุดการค้นหาได้
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
เมื่อหยุดการค้นหา การเชื่อมต่อที่มีอยู่จะไม่ได้รับผลกระทบ ผู้ส่งจะ
ส่ง Payload ไปยังผู้รับที่เชื่อมต่อได้ต่อไป
(ผู้ส่ง) ขอเชื่อมต่อ
เมื่อตั้งค่า Flag ทั้งหมดของผู้รับแล้ว ผู้ส่งจะขอเชื่อมต่อกับผู้รับได้
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() เป็นวิธีการที่แยกออกมาและแอปไคลเอ็นต์ต้องติดตั้งใช้งาน
เมื่อผู้รับยอมรับคำขอเชื่อมต่อ ระบบจะเรียกใช้
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);
}
}
ผู้ส่งสามารถใส่Binderออบเจ็กต์หรืออาร์เรย์ไบต์ในPayloadได้ หากผู้ส่งต้องการส่งข้อมูลประเภทอื่น จะต้องแปลงข้อมูลเป็นอาร์เรย์ไบต์ ใช้ไบต์อาร์เรย์เพื่อสร้างออบเจ็กต์ Payload และส่ง Payload จากนั้นไคลเอ็นต์ผู้รับจะได้รับอาร์เรย์ไบต์จาก Payload ที่ได้รับ และยกเลิกการซีเรียลไลซ์อาร์เรย์ไบต์เป็นออบเจ็กต์ข้อมูลที่คาดไว้
ตัวอย่างเช่น หากผู้ส่งต้องการส่งสตริง hello ไปยังปลายทางผู้รับที่มีรหัส FragmentB ผู้ส่งสามารถใช้ Proto Buffers เพื่อกำหนดประเภทข้อมูลได้ดังนี้
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
รูปที่ 1 แสดงโฟลว์ของ Payload
(บริการตัวรับ) รับและส่งเพย์โหลด
เมื่อแอปตัวรับได้รับ Payload ระบบจะเรียกใช้ AbstractReceiverService.onPayloadReceived()
ตามที่อธิบายไว้ในส่งเพย์โหลด onPayloadReceived() เป็น
เมธอดที่แยกออกมาและแอปไคลเอ็นต์ต้องใช้ ในเมธอดนี้ ไคลเอ็นต์สามารถส่งต่อ Payload ไปยังปลายทางตัวรับที่เกี่ยวข้อง หรือแคช Payload แล้วส่งเมื่อลงทะเบียนปลายทางตัวรับที่คาดไว้
(ปลายทางตัวรับ) ลงทะเบียนและยกเลิกการลงทะเบียน
แอปตัวรับสัญญาณควรเรียกใช้ registerReceiver() เพื่อลงทะเบียนปลายทางของตัวรับสัญญาณ
Use Case ทั่วไปคือ Fragment ต้องรับ 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เมื่อได้รับPayloadAbstractReceiverServiceในเครื่องรับจะเรียกใช้forwardPayload("FragmentB", payload)เพื่อส่งเพย์โหลดไปยังFragmentB - ผู้ส่งระบุ
data_type:VOLUME_CONTROLในPayloadเมื่อได้รับPayloadAbstractReceiverServiceในเครื่องรับจะทราบ ว่าควรส่งPayloadประเภทนี้ไปยังFragmentBจึงเรียกใช้forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(ผู้ส่ง) สิ้นสุดการเชื่อมต่อ
เมื่อผู้ส่งไม่จำเป็นต้องส่ง Payload ไปยังผู้รับอีกต่อไป (เช่น
ไม่มีการใช้งานแล้ว) ผู้ส่งควรสิ้นสุดการเชื่อมต่อ
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และCarOccupantConnectionServiceadb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService
Null CarRemoteDeviceManager และ CarOccupantConnectionManager
ดูสาเหตุที่เป็นไปได้ต่อไปนี้
บริการรถยนต์ขัดข้อง ดังที่แสดงไว้ก่อนหน้านี้ เราตั้งใจรีเซ็ตตัวจัดการทั้ง 2 รายให้เป็น
nullเมื่อบริการรถยนต์ขัดข้อง เมื่อรีสตาร์ทบริการรถยนต์ ระบบจะตั้งค่าตัวจัดการทั้ง 2 เป็นค่าที่ไม่ใช่ 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]โดยค่าเริ่มต้น ระบบจะปิดใช้บริการทั้ง 2 รายการนี้ เมื่ออุปกรณ์รองรับ การแสดงผลหลายจอ คุณต้องวางซ้อนไฟล์การกำหนดค่านี้ คุณเปิดใช้บริการทั้ง 2 รายการได้ในไฟล์การกำหนดค่า
// 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()อินสแตนซ์นี้ไม่ได้ลงทะเบียนStateCallbackCarRemoteDeviceManagerregisterReceiver()receiverEndpointIdลงทะเบียนแล้วunregisterReceiver()receiverEndpointIdไม่ได้จดทะเบียนrequestConnection()มีการเชื่อมต่อที่รอดำเนินการหรือสร้างไว้แล้วcancelConnection()ไม่มีการเชื่อมต่อที่รอดำเนินการซึ่งต้องยกเลิกsendPayload()ไม่มีการเชื่อมต่อdisconnect()ไม่มีการเชื่อมต่อ
ไคลเอ็นต์ 1 ส่งเพย์โหลดไปยังไคลเอ็นต์ 2 ได้ แต่ไคลเอ็นต์ 2 ส่งไปยังไคลเอ็นต์ 1 ไม่ได้
การเชื่อมต่อได้รับการออกแบบมาให้เป็นแบบทางเดียว หากต้องการสร้างการเชื่อมต่อแบบ 2 ทาง ทั้ง
client1 และ client2 ต้องขอเชื่อมต่อกันและ
รับการอนุมัติ