แอปที่มีสิทธิ์ของระบบใน 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
can จะลบล้างเมธอดนี้เพื่อเปิดใช้งานกิจกรรมการให้สิทธิ์ และเรียกใช้ 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 เพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดียิ่งขึ้น เช่น หากไม่ได้ตั้งค่า 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
เมื่อได้รับPayload
AbstractReceiverService
ในเครื่องรับจะเรียกใช้forwardPayload("FragmentB", payload)
เพื่อส่ง Payload ไปยังFragmentB
- ผู้ส่งระบุ
data_type:VOLUME_CONTROL
ในPayload
เมื่อได้รับPayload
AbstractReceiverService
ในเครื่องรับจะทราบ ว่าควรส่ง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
และCarOccupantConnectionService
adb 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()
ไม่มีการลงทะเบียนStateCallback
โดยอินสแตนซ์CarRemoteDeviceManager
นี้registerReceiver()
receiverEndpointId
ลงทะเบียนแล้วunregisterReceiver()
receiverEndpointId
ไม่ได้ลงทะเบียนrequestConnection()
มีการเชื่อมต่อที่รอดำเนินการหรือที่สร้างไว้แล้วcancelConnection()
ไม่มีการเชื่อมต่อที่รอดำเนินการซึ่งจะยกเลิกได้sendPayload()
ไม่มีการเชื่อมต่อที่สร้างไว้disconnect()
ไม่มีการเชื่อมต่อที่สร้างไว้
ไคลเอ็นต์ 1 ส่งเพย์โหลดไปยังไคลเอ็นต์ 2 ได้ แต่ไคลเอ็นต์ 2 ส่งเพย์โหลดไปยังไคลเอ็นต์ 1 ไม่ได้
การเชื่อมต่อได้รับการออกแบบมาให้เป็นแบบทางเดียว หากต้องการสร้างการเชื่อมต่อแบบ 2 ทาง ทั้ง
client1
และ client2
ต้องขอเชื่อมต่อกันและ
รับการอนุมัติ