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

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

이 API 세트는 이전 과정에서 설명한 Network Service Discovery API와 목적이 비슷하지만 코드로 구현하는 것은 매우 다릅니다. 이 과정에서는 Wi-Fi Direct를 사용하여 다른 기기에서 사용 가능한 서비스를 검색하는 방법을 보여줍니다. 이 과정에서는 개발자가 Wi-Fi Direct API를 이미 잘 알고 있다고 가정합니다.

매니페스트 설정

Wi-Fi P2P를 사용하려면 CHANGE_WIFI_STATE, ACCESS_WIFI_STATE, ACCESS_FINE_LOCATIONINTERNET 권한을 매니페스트에 추가합니다. 앱이 Android 13 (API 수준 33) 이상을 타겟팅한다면 매니페스트에 NEARBY_WIFI_DEVICES 권한도 추가합니다. Wi-Fi Direct는 인터넷 연결이 필요하지 않지만 표준 자바 소켓을 사용하므로 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를 만들어 수신 레코드를 수신 대기합니다. 이 레코드는 다른 기기에서 선택적으로 브로드캐스트할 수 있습니다. 하나가 들어오면 기기 주소와 원하는 기타 관련 정보를 현재 메서드 외부의 데이터 구조에 복사하여 나중에 액세스할 수 있습니다. 다음 예에서는 레코드에 사용자 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
        }
    }
}

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 레코드를 해당 서비스 정보와 연결합니다. 두 리스너가 모두 구현되면 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)
    ...
}

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

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