Multi-Display Communications API สามารถใช้โดยแอปที่ได้รับสิทธิ์ของระบบใน AAOS เพื่อสื่อสารกับแอปเดียวกัน (ชื่อแพ็กเกจเดียวกัน) ที่ทำงานอยู่ในแอปอื่น โซนผู้โดยสารในรถ หน้านี้จะอธิบายวิธีผสานรวม API เพื่อเรียนรู้ อื่นๆ คุณยังจะเห็น CarOccupantZoneManager.OccupantZoneInfo
โซนผู้พักอาศัย
แนวคิดของโซนผู้พักอาศัยจะจับคู่ผู้ใช้กับชุดจอแสดงผล ชิ้น โซนผู้โดยสารมีจอแสดงผลที่ระบุประเภท DISPLAY_TYPE_MAIN โซนผู้โดยสารอาจมีจอแสดงผลเพิ่มเติมด้วย เช่น คลัสเตอร์จอแสดงผล โซนผู้โดยสารแต่ละโซนจะได้รับการกำหนดผู้ใช้ Android ผู้ใช้แต่ละคนมีบัญชีของตนเอง และแอปพลิเคชันต่างๆ
การกำหนดค่าฮาร์ดแวร์
Comms API รองรับ SoC เดียวเท่านั้น ในโมเดล SoC เดียว ผู้โดยสารทั้งหมด และผู้ใช้ทำงานใน SoC เดียวกัน Comms API ประกอบด้วย 3 องค์ประกอบดังนี้
Power Management API จะช่วยให้ไคลเอ็นต์สามารถจัดการพลังงานของ แสดงในโซนที่มีการเข้าใช้
Discovery API ช่วยให้ลูกค้าสามารถตรวจสอบสถานะของผู้เช่ารายอื่นๆ ในรถ และเพื่อตรวจสอบไคลเอ็นต์เพียร์ในโซนที่มีการเข้าใช้ ใช้ Discovery API ก่อนที่จะใช้ Connection API
API การเชื่อมต่อทำให้ไคลเอ็นต์เชื่อมต่อกับเพียร์ไคลเอ็นต์ใน โซนที่มีการเข้าใช้อีกโซนและส่งเพย์โหลดไปยังไคลเอ็นต์การเพียร์
ต้องมี Discovery API และ Connection API สำหรับการเชื่อมต่อ พลัง 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
ช่วยให้มั่นใจว่าจะมีเฉพาะเฟรมเวิร์กเท่านั้นที่ผูกกับบริการนี้ได้ หากบริการนี้
ไม่ต้องใช้สิทธิ์ แอปอื่นอาจเชื่อมโยงกับ
และส่ง 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);
}
ก่อนที่จะขอการเชื่อมต่อกับผู้รับ ผู้ส่งควรตรวจสอบให้แน่ใจว่า มีการตั้งค่าแฟล็กของโซนควบคุมตัวรับและแอปตัวรับ หรือไม่เช่นนั้น อาจเกิดขึ้นได้ เช่น
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
) ด้วยเหตุนี้ แอปจึงไม่ต้องยืนยันว่า สองค่าตกลงกัน
เพื่อประสบการณ์ของผู้ใช้ที่ดียิ่งขึ้น ไคลเอ็นต์ผู้ส่ง CAN จะแสดง UI หากไม่มี Flag
ตั้งค่า ตัวอย่างเช่น ถ้าไม่ได้ตั้งค่า FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
ผู้ส่ง
สามารถแสดงข้อความโทสต์หรือกล่องโต้ตอบเพื่อแจ้งให้ผู้ใช้ปลดล็อกหน้าจอ
โซนตรวจจับของผู้รับ
เมื่อผู้ส่งไม่ต้องการค้นหาผู้รับอีกต่อไป (เช่น เมื่อ ค้นหาตัวรับสัญญาณและการเชื่อมต่อที่มีอยู่ทั้งหมด หรือไม่ทำงาน) หยุดการค้นพบ
if (mRemoteDeviceManager != null) {
mRemoteDeviceManager.unregisterStateCallback();
}
เมื่อหยุดการค้นพบ การเชื่อมต่อที่มีอยู่จะไม่ได้รับผลกระทบ ผู้ส่งสามารถ
ส่ง Payload
ไปยังเครื่องรับที่เชื่อมต่ออยู่ต่อไป
(ผู้ส่ง) ขอการเชื่อมต่อ
เมื่อตั้งค่าธงทั้งหมดของผู้รับแล้ว ผู้ส่งจะส่งคำขอเชื่อมต่อได้ ถึงผู้รับ:
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()
จากนั้นการเชื่อมต่อ
แล้ว
(ผู้ส่ง) ส่งเพย์โหลด
เมื่อเชื่อมต่อแล้ว ผู้ส่ง CAN จะส่ง 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
สามารถใช้บัฟเฟอร์ของโปรโตเพื่อกำหนดประเภทข้อมูล
ดังนี้
message MyData {
required string receiver_endpoint_id = 1;
required string data = 2;
}
รูปที่ 1 แสดงขั้นตอน Payload
(บริการตัวรับ) รับและส่งเพย์โหลด
เมื่อแอปที่เป็นผู้รับได้รับ Payload
ระบบจะเรียกใช้ AbstractReceiverService.onPayloadReceived()
ตามที่อธิบายไว้ใน
ส่งเพย์โหลด onPayloadReceived()
คือ
Abstracted Method และ "ต้อง" นำมาใช้โดยแอปไคลเอ็นต์ ในวิธีนี้ ฟิลด์
ไคลเอ็นต์ CAN ส่งต่อ Payload
ไปยังปลายทางของผู้รับที่เกี่ยวข้อง หรือ
แคช Payload
แล้วส่งเมื่อปลายทางผู้รับที่คาดไว้คือ
ที่ลงทะเบียนแล้ว
(ปลายทางผู้รับ) ลงทะเบียนและยกเลิกการลงทะเบียน
แอปตัวรับควรโทรหา registerReceiver()
เพื่อลงทะเบียนผู้รับ
ปลายทาง กรณีการใช้งานทั่วไปคือ Fragment ต้องได้รับ Payload
ดังนั้น
อุปกรณ์จะลงทะเบียนอุปกรณ์รับสัญญาณ
private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
…
};
if (mOccupantConnectionManager != null) {
mOccupantConnectionManager.registerReceiver("FragmentB",
getActivity().getMainExecutor(), mPayloadCallback);
}
เมื่อ AbstractReceiverService
ในไคลเอ็นต์ตัวรับส่ง
Payload
ไปยังปลายทางของผู้รับ PayloadCallback
ที่เชื่อมโยงจะเป็น
เรียกใช้
แอปไคลเอ็นต์ CAN จะลงทะเบียนเครื่องรับปลายทางหลายตัวตาม
receiverEndpointId
ไม่ซ้ำกันในแอปไคลเอ็นต์ receiverEndpointId
AbstractReceiverService
จะใช้ในการตัดสินใจว่าจะใช้รีซีฟเวอร์ใด
ปลายทางที่จะส่งเพย์โหลดไป เช่น
- ผู้ส่งระบุ
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
ไปยังผู้รับอีกต่อไป (เช่น
ไม่ทำงาน) การเชื่อมต่อจะยุติลง
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
เมื่อบริการรถยนต์ขัดข้อง เวลาที่บริการรถยนต์ มีการเริ่มต้นใหม่ ผู้จัดการทั้งสองจะถูกตั้งค่าเป็นค่าที่ไม่ใช่ค่าว่าง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 รายการในไฟล์การกำหนดค่า
// 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()
ไม่มีการเชื่อมต่อที่สร้างขึ้น
Client1 สามารถส่งเพย์โหลดไปยัง client2 ได้ แต่ส่งเพย์โหลดไปยัง client2 ไม่ได้
การเชื่อมต่อออกแบบมาเพียงวิธีเดียว หากต้องการสร้างการเชื่อมต่อแบบ 2 ทาง ให้ทำดังนี้
client1
และ client2
ต้องขอการเชื่อมต่อระหว่างกัน จากนั้น
ขออนุมัติ