Использовать Wi-Fi Direct (P2P) для обнаружения сервисов

На первом уроке этого курса, Использование обнаружения сетевых сервисов, вы узнали, как обнаруживать сервисы, подключенные к локальной сети. Однако с помощью обнаружения сервисов Wi-Fi Direct (P2P) вы можете обнаруживать сервисы устройств поблизости напрямую, без подключения к сети. Вы также можете рекламировать сервисы, работающие на вашем устройстве. Эти возможности позволяют обмениваться данными между приложениями, даже если локальная сеть или точка доступа недоступны.

Хотя этот набор API похож по назначению на API обнаружения сетевых сервисов, описанные в предыдущем уроке, их реализация в коде сильно отличается. В этом уроке показано, как обнаруживать сервисы, доступные на других устройствах, с помощью Wi-Fi Direct. Предполагается, что вы уже знакомы с API Wi-Fi Direct.

Настройка манифеста

Чтобы использовать 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
            }
        })
    }

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 с соответствующей информацией о сервисе. После реализации обоих прослушивателей добавьте их в 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(). Этот метод также принимает прослушиватель, чтобы сообщать об успехе или неудаче.

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
Не удалось выполнить операцию из-за внутренней ошибки.