서비스 검색을 위해 Wi-Fi Direct(P2P) 사용

이 과정의 첫 번째 강의인 네트워크 서비스 검색 사용에서는 로컬 네트워크에 연결된 서비스를 검색하는 방법을 보여주었습니다. 그러나 Wi-Fi Direct (P2P) 서비스 검색을 사용하면 네트워크에 연결하지 않고도 근처 기기의 서비스를 직접 검색할 수 있습니다. 기기에서 실행 중인 서비스를 알릴 수도 있습니다. 이러한 기능을 사용하면 로컬 네트워크나 핫스팟을 사용할 수 없는 경우에도 앱 간에 통신할 수 있습니다.

이 API 집합은 이전 강의에서 설명한 네트워크 서비스 검색 API와 목적이 유사하지만 코드에서 구현하는 방식은 매우 다릅니다. 이 과정에서는 Wi-Fi Direct를 사용하여 다른 기기에서 사용할 수 있는 서비스를 검색하는 방법을 보여줍니다. 이 과정에서는 개발자가 Wi-Fi Direct API에 이미 익숙하다고 가정합니다.

매니페스트 설정

Wi-Fi P2P를 사용하려면 CHANGE_WIFI_STATE, ACCESS_WIFI_STATE, ACCESS_FINE_LOCATION, INTERNET 권한을 매니페스트에 추가합니다. 앱이 Android 13 (API 수준 33) 이상을 타겟팅하는 경우 NEARBY_WIFI_DEVICES 권한도 매니페스트에 추가합니다. 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
            }
        })
    }

자바

    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를 만듭니다. 이 레코드는 다른 기기에서 브로드캐스트할 수 있습니다(선택사항). 수신되면 기기 주소와 원하는 기타 관련 정보를 현재 메서드 외부의 데이터 구조에 복사하여 나중에 액세스할 수 있도록 합니다. 다음 예시에서는 레코드에 사용자의 ID로 채워진 '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
        }
    }
}

자바

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 레코드를 해당 서비스 정보와 연결합니다. 두 리스너가 모두 구현되면 setDnsSdResponseListeners() 메서드를 사용하여 WifiP2pManager에 추가합니다.

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)
    ...
}

자바

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()를 호출합니다. 이 메서드는 리스너를 사용하여 성공 또는 실패도 보고합니다.

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
                    }
                }
        )

자바

        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.")
                            }
                        }
                    }
                }
        )

자바

        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
내부 오류로 인해 작업이 실패했습니다.