แอปที่มีสิทธิ์ระดับระบบใน AAOS สามารถใช้ Multi-Display Communications API เพื่อสื่อสารกับแอปเดียวกัน (ชื่อแพ็กเกจเดียวกัน) ที่ทำงานในโซนที่มีการเข้าใช้อื่นในรถ หน้านี้จะอธิบายวิธีผสานรวม API ดูข้อมูลเพิ่มเติมได้ใน CarOccupantZoneManager.OccupantZoneInfo
โซนผู้โดยสาร
แนวคิดของโซนผู้อยู่อาศัยจะจับคู่ผู้ใช้กับชุดจอแสดงผล โซนผู้อยู่อาศัยแต่ละโซนจะมีจอแสดงผลประเภท DISPLAY_TYPE_MAIN โซนผู้อาศัยอาจมีจอแสดงผลเพิ่มเติมด้วย เช่น จอแสดงคลัสเตอร์ โซนผู้อยู่อาศัยแต่ละโซนจะมีผู้ใช้ Android กำหนดไว้ ผู้ใช้แต่ละรายจะมีบัญชีและแอปของตนเอง
การกำหนดค่าฮาร์ดแวร์
Comms API รองรับ SoC เพียงตัวเดียว ในรุ่น SoC เดียว โซนและผู้ที่อยู่ในอาคารทั้งหมดจะทำงานบน SoC เดียวกัน Comms API ประกอบด้วยคอมโพเนนต์ 3 รายการ ได้แก่
Power Management 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()is invoked when
MyReceiverServicehas received a
Payloadfrom the sender client.
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
permission
ช่วยให้มั่นใจได้ว่ามีเพียงเฟรมเวิร์กเท่านั้นที่จะเชื่อมโยงกับบริการนี้ได้ หากบริการนี้ไม่จําเป็นต้องใช้สิทธิ์ แอปอื่นอาจเชื่อมโยงกับบริการนี้และส่ง 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);
(ผู้ส่ง) Discover
ก่อนเชื่อมต่อกับไคลเอ็นต์ผู้รับ ไคลเอ็นต์ผู้ส่งควรค้นหาไคลเอ็นต์ผู้รับโดยการลงทะเบียน 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);
}
ก่อนขอเชื่อมต่อกับผู้รับ ผู้ส่งควรตรวจสอบว่าได้ตั้งค่า Flag ทั้งหมดของโซนผู้ใช้งานและผู้รับแอปแล้ว มิฉะนั้น อาจเกิดข้อผิดพลาดได้ เช่น
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 ทั้งหมดของผู้รับแล้วเท่านั้น อย่างไรก็ตาม มีข้อยกเว้นดังนี้
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()
ดังที่อธิบายไว้ใน(Sender) Request Connection
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 ทั่วไปคือ ข้อมูลโค้ดย่อยต้องรับ Payload
ดังนั้นจึงลงทะเบียนปลายทางของผู้รับ ดังนี้
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
เมื่อ AbstractReceiverService
ในไคลเอ็นต์ฝั่งผู้รับส่ง Payload
ไปยังปลายทางฝั่งผู้รับ ระบบจะเรียกใช้ PayloadCallback
ที่เชื่อมโยง
แอปไคลเอ็นต์สามารถลงทะเบียนปลายทางผู้รับได้หลายรายการ ตราบใดที่ receiverEndpointId
ของแอปไคลเอ็นต์ไม่ซ้ำกัน AbstractReceiverService
จะใช้ receiverEndpointId
เพื่อตัดสินใจว่าปลายทางผู้รับใดที่จะส่งเพย์โหลดไป เช่น
- ผู้ส่งระบุ
receiver_endpoint_id:FragmentB
ในPayload
เมื่อได้รับPayload
แล้วAbstractReceiverService
ในเครื่องรับจะเรียกใช้forwardPayload("FragmentB", payload)
เพื่อส่งเพย์โหลดไปยังFragmentB
- ผู้ส่งระบุ
data_type:VOLUME_CONTROL
ในPayload
เมื่อได้รับPayload
AbstractReceiverService
ในเครื่องรับจะทราบว่าควรส่งPayload
ประเภทนี้ไปยังFragmentB
จึงเรียกใช้forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.unregisterReceiver("FragmentB");
}
(ผู้ส่ง) สิ้นสุดการเชื่อมต่อ
เมื่อผู้ส่งไม่จำเป็นต้องส่ง Payload
ไปยังผู้รับอีกต่อไป (เช่น 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 รายการเป็นค่าที่ไม่ใช่ NullCarRemoteDeviceService
หรือ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()
อินสแตนซ์CarRemoteDeviceManager
นี้ไม่ได้ลงทะเบียนStateCallback
registerReceiver()
receiverEndpointId
มีการจดทะเบียนอยู่แล้วunregisterReceiver()
receiverEndpointId
ไม่ได้จดทะเบียนrequestConnection()
มีการเชื่อมต่อที่รอดำเนินการหรือเชื่อมต่อแล้วcancelConnection()
ไม่มีการเชื่อมต่อที่รอดำเนินการเพื่อยกเลิกsendPayload()
ไม่มีการเชื่อมต่อdisconnect()
ไม่มีการเชื่อมต่อ
ไคลเอ็นต์ 1 ส่งเพย์โหลดไปยังไคลเอ็นต์ 2 ได้ แต่ในทางกลับกันไม่ได้
การเชื่อมต่อเป็นแบบทางเดียวโดยการออกแบบ หากต้องการสร้างการเชื่อมต่อแบบ 2 ทาง ทั้ง client1
และ client2
จะต้องขอเชื่อมต่อกันและรับการอนุมัติ