ใช้ Wi-Fi Direct (P2P) เพื่อค้นหาบริการ

บทเรียนแรกในชั้นเรียนนี้อย่างการใช้การสำรวจบริการเครือข่ายได้แสดงวิธีสำรวจบริการที่เชื่อมต่อกับเครือข่ายภายใน อย่างไรก็ตาม การใช้การสำรวจบริการ Wi-Fi Direct (P2P) ช่วยให้คุณค้นพบบริการของอุปกรณ์ที่อยู่ใกล้เคียงได้โดยตรงโดยไม่ต้องเชื่อมต่อเครือข่าย นอกจากนี้คุณยังสามารถโฆษณาบริการ ที่ทำงานในอุปกรณ์ของคุณ ความสามารถเหล่านี้ช่วยให้คุณสื่อสารระหว่างแอปได้แม้ว่าจะไม่มีเครือข่ายภายในหรือฮอตสปอตก็ตาม

แม้ว่า API ชุดนี้จะมีจุดประสงค์ที่คล้ายกับ Network Service Discovery API ที่ระบุไว้ในบทเรียนก่อนหน้านี้ แต่การนำ API เหล่านี้ไปใช้ในโค้ดนั้นแตกต่างกันอย่างมาก บทเรียนนี้จะแสดงวิธีค้นหาบริการที่พร้อมใช้งานจากอุปกรณ์อื่นๆ โดยใช้ Wi-Fi Direct บทเรียนนี้จะถือว่าคุณคุ้นเคยกับ API ของ Wi-Fi Direct อยู่แล้ว

ตั้งค่าไฟล์ Manifest

หากต้องการใช้ Wi-Fi P2P ให้เพิ่มสิทธิ์ CHANGE_WIFI_STATE, ACCESS_WIFI_STATE, ACCESS_FINE_LOCATION และ INTERNET ลงในไฟล์ Manifest หากแอปกำหนดเป้าหมายเป็น Android 13 (API ระดับ 33) ขึ้นไป ให้เพิ่มสิทธิ์ NEARBY_WIFI_DEVICES ลงในไฟล์ Manifest ด้วย แม้ว่า Wi-Fi Direct จะไม่จำเป็นต้องใช้การเชื่อมต่ออินเทอร์เน็ต แต่ระบบจะใช้ซ็อกเก็ต Java มาตรฐาน และการใช้อุปกรณ์เหล่านี้ใน Android ก็จำเป็นต้องมีการอนุญาตที่ขอ

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.nsdchat"
    ...

    <uses-permission
        android:required="true"
        android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.INTERNET"/>
    <!-- If your app targets Android 13 (API level 33)
         or higher, you must declare the NEARBY_WIFI_DEVICES permission. -->
    <uses-permission
        android:name="android.permission.NEARBY_WIFI_DEVICES"
        <!-- If your app derives location information from Wi-Fi APIs,
             don't include the "usesPermissionFlags" attribute. -->
        android:usesPermissionFlags="neverForLocation" />
    <uses-permission
        android:required="true"
        android:name="android.permission.ACCESS_FINE_LOCATION"
        <!-- If any feature in your app relies on precise location information,
             don't include the "maxSdkVersion" attribute. -->
        android:maxSdkVersion="32" />
    ...

นอกจากสิทธิ์ข้างต้นแล้ว API ต่อไปนี้ยังกำหนดให้ต้องเปิดใช้โหมดตำแหน่งด้วย

เพิ่มบริการในพื้นที่

หากคุณให้บริการในพื้นที่ คุณต้องลงทะเบียนบริการเพื่อรับการค้นพบบริการ เมื่อลงทะเบียนบริการในพื้นที่แล้ว เฟรมเวิร์กจะตอบกลับคำขอค้นพบบริการจากแอปเทียบเท่าโดยอัตโนมัติ

วิธีสร้างบริการในพื้นที่

  1. สร้างออบเจ็กต์ WifiP2pServiceInfo
  2. ใส่ข้อมูลเกี่ยวกับบริการของคุณลงไป
  3. โทรหา addLocalService() เพื่อลงทะเบียนบริการในพื้นที่สำหรับการค้นหาบริการ

Kotlin

    private fun startRegistration() {
        //  Create a string map containing information about your service.
        val record: Map<String, String> = mapOf(
                "listenport" to SERVER_PORT.toString(),
                "buddyname" to "John Doe${(Math.random() * 1000).toInt()}",
                "available" to "visible"
        )

        // Service information.  Pass it an instance name, service type
        // _protocol._transportlayer , and the map containing
        // information other devices will want once they connect to this one.
        val serviceInfo =
                WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record)

        // Add the local service, sending the service info, network channel,
        // and listener that will be used to indicate success or failure of
        // the request.
        manager.addLocalService(channel, serviceInfo, object : WifiP2pManager.ActionListener {
            override fun onSuccess() {
                // Command successful! Code isn't necessarily needed here,
                // Unless you want to update the UI or add logging statements.
            }

            override fun onFailure(arg0: Int) {
                // Command failed.  Check for P2P_UNSUPPORTED, ERROR, or BUSY
            }
        })
    }

Java

    private void startRegistration() {
        //  Create a string map containing information about your service.
        Map record = new HashMap();
        record.put("listenport", String.valueOf(SERVER_PORT));
        record.put("buddyname", "John Doe" + (int) (Math.random() * 1000));
        record.put("available", "visible");

        // Service information.  Pass it an instance name, service type
        // _protocol._transportlayer , and the map containing
        // information other devices will want once they connect to this one.
        WifiP2pDnsSdServiceInfo serviceInfo =
                WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record);

        // Add the local service, sending the service info, network channel,
        // and listener that will be used to indicate success or failure of
        // the request.
        manager.addLocalService(channel, serviceInfo, new ActionListener() {
            @Override
            public void onSuccess() {
                // Command successful! Code isn't necessarily needed here,
                // Unless you want to update the UI or add logging statements.
            }

            @Override
            public void onFailure(int arg0) {
                // Command failed.  Check for P2P_UNSUPPORTED, ERROR, or BUSY
            }
        });
    }

สำรวจบริการที่อยู่ใกล้เคียง

Android ใช้เมธอดการเรียกกลับเพื่อแจ้งให้แอปพลิเคชันทราบถึงบริการที่ใช้ได้ ดังนั้นสิ่งแรกที่ต้องทำคือตั้งค่าเมธอดเหล่านั้น สร้าง WifiP2pManager.DnsSdTxtRecordListener เพื่อรอรับระเบียนขาเข้า อุปกรณ์อื่นๆ สามารถออกอากาศระเบียนนี้ได้โดยไม่ได้บังคับ เมื่อได้รับคำขอแล้ว ให้คัดลอกที่อยู่อุปกรณ์และข้อมูลอื่นๆ ที่เกี่ยวข้องที่ต้องการไปยังโครงสร้างข้อมูลที่อยู่นอกเมธอดปัจจุบัน เพื่อให้เข้าถึงข้อมูลดังกล่าวในภายหลังได้ ตัวอย่างต่อไปนี้สมมติว่าระเบียนมีช่อง "buddyname" ซึ่งป้อนข้อมูลระบุตัวตนของผู้ใช้

Kotlin

private val buddies = mutableMapOf<String, String>()
...
private fun discoverService() {
    /* Callback includes:
     * fullDomain: full domain name: e.g. "printer._ipp._tcp.local."
     * record: TXT record dta as a map of key/value pairs.
     * device: The device running the advertised service.
     */
    val txtListener = DnsSdTxtRecordListener { fullDomain, record, device ->
        Log.d(TAG, "DnsSdTxtRecord available -$record")
        record["buddyname"]?.also {
            buddies[device.deviceAddress] = it
        }
    }
}

Java

final HashMap<String, String> buddies = new HashMap<String, String>();
...
private void discoverService() {
    DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() {
        @Override
        /* Callback includes:
         * fullDomain: full domain name: e.g. "printer._ipp._tcp.local."
         * record: TXT record dta as a map of key/value pairs.
         * device: The device running the advertised service.
         */

        public void onDnsSdTxtRecordAvailable(
                String fullDomain, Map record, WifiP2pDevice device) {
                Log.d(TAG, "DnsSdTxtRecord available -" + record.toString());
                buddies.put(device.deviceAddress, record.get("buddyname"));
            }
        };
}

หากต้องการดูข้อมูลบริการ ให้สร้าง WifiP2pManager.DnsSdServiceResponseListener ซึ่งจะได้รับคำอธิบายและข้อมูลการเชื่อมต่อจริง ข้อมูลโค้ดก่อนหน้านี้ใช้ออบเจ็กต์ Map เพื่อจับคู่ที่อยู่อุปกรณ์กับชื่อเพื่อน ผู้รับฟังการตอบกลับของบริการจะใช้ข้อมูลนี้เพื่อลิงก์ระเบียน DNS กับข้อมูลบริการที่เกี่ยวข้อง เมื่อติดตั้งใช้งานทั้ง 2 รายการแล้ว ให้เพิ่มลงใน WifiP2pManager โดยใช้เมธอด setDnsSdResponseListeners()

Kotlin

private fun discoverService() {
    ...

    val servListener = DnsSdServiceResponseListener { instanceName, registrationType, resourceType ->
        // Update the device name with the human-friendly version from
        // the DnsTxtRecord, assuming one arrived.
        resourceType.deviceName = buddies[resourceType.deviceAddress] ?: resourceType.deviceName

        // Add to the custom adapter defined specifically for showing
        // wifi devices.
        val fragment = fragmentManager
                .findFragmentById(R.id.frag_peerlist) as WiFiDirectServicesList
        (fragment.listAdapter as WiFiDevicesAdapter).apply {
            add(resourceType)
            notifyDataSetChanged()
        }

        Log.d(TAG, "onBonjourServiceAvailable $instanceName")
    }

    manager.setDnsSdResponseListeners(channel, servListener, txtListener)
    ...
}

Java

private void discoverService() {
...

    DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() {
        @Override
        public void onDnsSdServiceAvailable(String instanceName, String registrationType,
                WifiP2pDevice resourceType) {

                // Update the device name with the human-friendly version from
                // the DnsTxtRecord, assuming one arrived.
                resourceType.deviceName = buddies
                        .containsKey(resourceType.deviceAddress) ? buddies
                        .get(resourceType.deviceAddress) : resourceType.deviceName;

                // Add to the custom adapter defined specifically for showing
                // wifi devices.
                WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager()
                        .findFragmentById(R.id.frag_peerlist);
                WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment
                        .getListAdapter());

                adapter.add(resourceType);
                adapter.notifyDataSetChanged();
                Log.d(TAG, "onBonjourServiceAvailable " + instanceName);
        }
    };

    manager.setDnsSdResponseListeners(channel, servListener, txtListener);
    ...
}

จากนั้นสร้างคำขอบริการและโทรหา addServiceRequest() วิธีนี้ทำให้ Listener รายงานสถานะสำเร็จหรือล้มเหลวด้วย

Kotlin

        serviceRequest = WifiP2pDnsSdServiceRequest.newInstance()
        manager.addServiceRequest(
                channel,
                serviceRequest,
                object : WifiP2pManager.ActionListener {
                    override fun onSuccess() {
                        // Success!
                    }

                    override fun onFailure(code: Int) {
                        // Command failed.  Check for P2P_UNSUPPORTED, ERROR, or BUSY
                    }
                }
        )

Java

        serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
        manager.addServiceRequest(channel,
                serviceRequest,
                new ActionListener() {
                    @Override
                    public void onSuccess() {
                        // Success!
                    }

                    @Override
                    public void onFailure(int code) {
                        // Command failed.  Check for P2P_UNSUPPORTED, ERROR, or BUSY
                    }
                });

สุดท้าย ให้โทรหา discoverServices()

Kotlin

        manager.discoverServices(
                channel,
                object : WifiP2pManager.ActionListener {
                    override fun onSuccess() {
                        // Success!
                    }

                    override fun onFailure(code: Int) {
                        // Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
                        when (code) {
                            WifiP2pManager.P2P_UNSUPPORTED -> {
                                Log.d(TAG, "Wi-Fi Direct isn't supported on this device.")
                            }
                        }
                    }
                }
        )

Java

        manager.discoverServices(channel, new ActionListener() {

            @Override
            public void onSuccess() {
                // Success!
            }

            @Override
            public void onFailure(int code) {
                // Command failed.  Check for P2P_UNSUPPORTED, ERROR, or BUSY
                if (code == WifiP2pManager.P2P_UNSUPPORTED) {
                    Log.d(TAG, "Wi-Fi Direct isn't supported on this device.");
                else if(...)
                    ...
            }
        });

หากทุกอย่างเป็นไปด้วยดี แสดงว่าเสร็จเรียบร้อย หากพบปัญหา โปรดทราบว่าการเรียกแบบไม่พร้อมกันที่คุณใช้จะใช้ WifiP2pManager.ActionListener เป็นอาร์กิวเมนต์ ซึ่งจะมีการเรียกกลับที่บ่งชี้ถึงความสำเร็จหรือความล้มเหลว หากต้องการวินิจฉัยปัญหา ให้ใส่รหัสการแก้ไขข้อบกพร่องใน onFailure() รหัสข้อผิดพลาดที่มาจากคำแนะนำของวิธีการของปัญหา ค่าข้อผิดพลาดที่เป็นไปได้และความหมายมีดังนี้

P2P_UNSUPPORTED
อุปกรณ์ที่ใช้แอปไม่รองรับ Wi-Fi Direct
BUSY
ระบบทำงานหนักเกินไปที่จะประมวลผลคำขอ
ERROR
การดำเนินการล้มเหลวเนื่องจากข้อผิดพลาดภายใน