Создавайте P2P-соединения с помощью Wi-Fi Direct

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

API-интерфейсы Wi-Fi Direct (P2P) позволяют приложениям подключаться к близлежащим устройствам без необходимости подключения к сети или точке доступа. Если ваше приложение предназначено для работы в защищенной сети ближнего радиуса действия, Wi-Fi Direct является более подходящим вариантом, чем традиционная одноранговая сеть Wi-Fi, по следующим причинам:

  • Wi-Fi Direct поддерживает шифрование WPA2. (Некоторые одноранговые сети поддерживают только шифрование WEP.)
  • Устройства могут транслировать предоставляемые ими услуги, что помогает другим устройствам легче находить подходящих одноранговых узлов.
  • При определении того, какое устройство должно быть владельцем группы в сети, Wi-Fi Direct проверяет управление питанием, пользовательский интерфейс и возможности обслуживания каждого устройства и использует эту информацию для выбора устройства, которое может наиболее эффективно выполнять обязанности сервера.
  • Android не поддерживает одноранговый режим Wi-Fi.

В этом уроке показано, как найти устройства поблизости и подключиться к ним с помощью Wi-Fi P2P.

Настройка разрешений приложения

Чтобы использовать Wi-Fi Direct, добавьте разрешения ACCESS_FINE_LOCATION , CHANGE_WIFI_STATE , ACCESS_WIFI_STATE и INTERNET в свой манифест. Если ваше приложение предназначено для Android 13 (уровень API 33) или выше, также добавьте разрешение NEARBY_WIFI_DEVICES в свой манифест. Wi-Fi Direct не требует подключения к Интернету, но использует стандартные сокеты Java, для которых требуется разрешение INTERNET . Итак, для использования Wi-Fi Direct вам потребуются следующие разрешения:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.nsdchat"
    ...
    <!-- 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" />
    <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"/>
    ...

Помимо предыдущих разрешений, следующие API также требуют включения режима местоположения:

Настройка приемника вещания и однорангового менеджера

Чтобы использовать Wi-Fi Direct, вам необходимо прослушивать широковещательные намерения, которые сообщают вашему приложению, когда произошли определенные события. В своем приложении создайте экземпляр IntentFilter и настройте его на прослушивание следующего:

WIFI_P2P_STATE_CHANGED_ACTION
Указывает, включен ли Wi-Fi Direct
WIFI_P2P_PEERS_CHANGED_ACTION
Указывает, что список доступных одноранговых узлов изменился.
WIFI_P2P_CONNECTION_CHANGED_ACTION
Указывает, что состояние подключения Wi-Fi Direct изменилось. Начиная с Android 10, это не прилипает. Если ваше приложение полагалось на получение этих широковещательных рассылок при регистрации, поскольку они были закреплены, используйте вместо этого соответствующий метод get при инициализации, чтобы получить информацию.
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
Указывает, что сведения о конфигурации этого устройства изменились. Начиная с Android 10, это не прилипает. Если ваше приложение полагалось на получение этих широковещательных рассылок при регистрации, поскольку они были закреплены, используйте вместо этого соответствующий метод get при инициализации, чтобы получить информацию.

Котлин

private val intentFilter = IntentFilter()
...
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main)

    // Indicates a change in the Wi-Fi Direct status.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)

    // Indicates a change in the list of available peers.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)

    // Indicates the state of Wi-Fi Direct connectivity has changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)

    // Indicates this device's details have changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)
    ...
}

Ява

private final IntentFilter intentFilter = new IntentFilter();
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // Indicates a change in the Wi-Fi Direct status.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);

    // Indicates a change in the list of available peers.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);

    // Indicates the state of Wi-Fi Direct connectivity has changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);

    // Indicates this device's details have changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
    ...
}

В конце метода onCreate() получите экземпляр WifiP2pManager и вызовите его метод initialize() . Этот метод возвращает объект WifiP2pManager.Channel , который вы будете использовать позже для подключения вашего приложения к платформе Wi-Fi Direct.

Котлин

private lateinit var channel: WifiP2pManager.Channel
private lateinit var manager: WifiP2pManager

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    manager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
    channel = manager.initialize(this, mainLooper, null)
}

Ява

Channel channel;
WifiP2pManager manager;

@Override
public void onCreate(Bundle savedInstanceState) {
    ...
    manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
    channel = manager.initialize(this, getMainLooper(), null);
}

Теперь создайте новый класс BroadcastReceiver , который вы будете использовать для прослушивания изменений состояния Wi-Fi системы. В методе onReceive() добавьте условие для обработки каждого изменения состояния, указанного выше.

Котлин

override fun onReceive(context: Context, intent: Intent) {
    when(intent.action) {
        WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
            // Determine if Wi-Fi Direct mode is enabled or not, alert
            // the Activity.
            val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)
            activity.isWifiP2pEnabled = state == WifiP2pManager.WIFI_P2P_STATE_ENABLED
        }
        WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {

            // The peer list has changed! We should probably do something about
            // that.

        }
        WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {

            // Connection state changed! We should probably do something about
            // that.

        }
        WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
            (activity.supportFragmentManager.findFragmentById(R.id.frag_list) as DeviceListFragment)
                    .apply {
                        updateThisDevice(
                                intent.getParcelableExtra(
                                        WifiP2pManager.EXTRA_WIFI_P2P_DEVICE) as WifiP2pDevice
                        )
                    }
        }
    }
}

Ява

@Override
public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
        // Determine if Wi-Fi Direct mode is enabled or not, alert
        // the Activity.
        int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
        if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
            activity.setIsWifiP2pEnabled(true);
        } else {
            activity.setIsWifiP2pEnabled(false);
        }
    } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

        // The peer list has changed! We should probably do something about
        // that.

    } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

        // Connection state changed! We should probably do something about
        // that.

    } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
        DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager()
                .findFragmentById(R.id.frag_list);
        fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra(
                WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));

    }
}

Наконец, добавьте код для регистрации фильтра намерений и широковещательного приемника, когда ваше основное действие активно, и отменяйте их регистрацию, когда действие приостановлено. Лучшее место для этого — методы onResume() и onPause() .

Котлин

/** register the BroadcastReceiver with the intent values to be matched  */
public override fun onResume() {
    super.onResume()
    receiver = WiFiDirectBroadcastReceiver(manager, channel, this)
    registerReceiver(receiver, intentFilter)
}

public override fun onPause() {
    super.onPause()
    unregisterReceiver(receiver)
}

Ява

/** register the BroadcastReceiver with the intent values to be matched */
@Override
public void onResume() {
    super.onResume();
    receiver = new WiFiDirectBroadcastReceiver(manager, channel, this);
    registerReceiver(receiver, intentFilter);
}

@Override
public void onPause() {
    super.onPause();
    unregisterReceiver(receiver);
}

Инициировать обнаружение пиров

Чтобы начать поиск ближайших устройств с помощью Wi-Fi P2P, вызовите discoverPeers() . Этот метод принимает следующие аргументы:

  • WifiP2pManager.Channel , полученный вами при инициализации однорангового mManager.
  • Реализация WifiP2pManager.ActionListener с методами, которые система вызывает для успешного и неудачного обнаружения.

Котлин

manager.discoverPeers(channel, object : WifiP2pManager.ActionListener {

    override fun onSuccess() {
        // Code for when the discovery initiation is successful goes here.
        // No services have actually been discovered yet, so this method
        // can often be left blank. Code for peer discovery goes in the
        // onReceive method, detailed below.
    }

    override fun onFailure(reasonCode: Int) {
        // Code for when the discovery initiation fails goes here.
        // Alert the user that something went wrong.
    }
})

Ява

manager.discoverPeers(channel, new WifiP2pManager.ActionListener() {

    @Override
    public void onSuccess() {
        // Code for when the discovery initiation is successful goes here.
        // No services have actually been discovered yet, so this method
        // can often be left blank. Code for peer discovery goes in the
        // onReceive method, detailed below.
    }

    @Override
    public void onFailure(int reasonCode) {
        // Code for when the discovery initiation fails goes here.
        // Alert the user that something went wrong.
    }
});

Имейте в виду, что это только инициирует обнаружение одноранговых узлов. Метод discoverPeers() запускает процесс обнаружения и затем немедленно завершает его. Система уведомляет вас, если процесс обнаружения одноранговых узлов успешно инициирован путем вызова методов в предоставленном прослушивателе действий. Кроме того, обнаружение остается активным до тех пор, пока не будет инициировано соединение или не сформирована группа P2P.

Получить список пиров

Теперь напишите код, который извлекает и обрабатывает список пиров. Сначала реализуйте интерфейс WifiP2pManager.PeerListListener , который предоставляет информацию об узлах, обнаруженных Wi-Fi Direct. Эта информация также позволяет вашему приложению определять, когда одноранговые узлы присоединяются к сети или покидают ее. Следующий фрагмент кода иллюстрирует эти операции, связанные с одноранговыми узлами:

Котлин

private val peers = mutableListOf<WifiP2pDevice>()
...

private val peerListListener = WifiP2pManager.PeerListListener { peerList ->
    val refreshedPeers = peerList.deviceList
    if (refreshedPeers != peers) {
        peers.clear()
        peers.addAll(refreshedPeers)

        // If an AdapterView is backed by this data, notify it
        // of the change. For instance, if you have a ListView of
        // available peers, trigger an update.
        (listAdapter as WiFiPeerListAdapter).notifyDataSetChanged()

        // Perform any other updates needed based on the new list of
        // peers connected to the Wi-Fi P2P network.
    }

    if (peers.isEmpty()) {
        Log.d(TAG, "No devices found")
        return@PeerListListener
    }
}

Ява

private List<WifiP2pDevice> peers = new ArrayList<WifiP2pDevice>();
...

private PeerListListener peerListListener = new PeerListListener() {
    @Override
    public void onPeersAvailable(WifiP2pDeviceList peerList) {

        List<WifiP2pDevice> refreshedPeers = peerList.getDeviceList();
        if (!refreshedPeers.equals(peers)) {
            peers.clear();
            peers.addAll(refreshedPeers);

            // If an AdapterView is backed by this data, notify it
            // of the change. For instance, if you have a ListView of
            // available peers, trigger an update.
            ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();

            // Perform any other updates needed based on the new list of
            // peers connected to the Wi-Fi P2P network.
        }

        if (peers.size() == 0) {
            Log.d(WiFiDirectActivity.TAG, "No devices found");
            return;
        }
    }
}

Теперь измените метод onReceive() вашего приемника вещания, чтобы он вызывал requestPeers() при получении намерения с действием WIFI_P2P_PEERS_CHANGED_ACTION . Вам нужно каким-то образом передать этот прослушиватель в приемник. Один из способов — отправить его в качестве аргумента конструктору приемника широковещательной передачи.

Котлин

fun onReceive(context: Context, intent: Intent) {
    when (intent.action) {
        ...
        WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {

            // Request available peers from the wifi p2p manager. This is an
            // asynchronous call and the calling activity is notified with a
            // callback on PeerListListener.onPeersAvailable()
            mManager?.requestPeers(channel, peerListListener)
            Log.d(TAG, "P2P peers changed")


        }
        ...
    }
}

Ява

public void onReceive(Context context, Intent intent) {
    ...
    else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

        // Request available peers from the wifi p2p manager. This is an
        // asynchronous call and the calling activity is notified with a
        // callback on PeerListListener.onPeersAvailable()
        if (mManager != null) {
            mManager.requestPeers(channel, peerListListener);
        }
        Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
    }...
}

Теперь намерение с действием WIFI_P2P_PEERS_CHANGED_ACTION инициирует запрос обновленного списка пиров.

Подключиться к пиру

Чтобы подключиться к одноранговому узлу, создайте новый объект WifiP2pConfig и скопируйте в него данные из WifiP2pDevice представляющего устройство, к которому вы хотите подключиться. Затем вызовите метод connect() .

Котлин

override fun connect() {
    // Picking the first device found on the network.
    val device = peers[0]

    val config = WifiP2pConfig().apply {
        deviceAddress = device.deviceAddress
        wps.setup = WpsInfo.PBC
    }

    manager.connect(channel, config, object : WifiP2pManager.ActionListener {

        override fun onSuccess() {
            // WiFiDirectBroadcastReceiver notifies us. Ignore for now.
        }

        override fun onFailure(reason: Int) {
            Toast.makeText(
                    this@WiFiDirectActivity,
                    "Connect failed. Retry.",
                    Toast.LENGTH_SHORT
            ).show()
        }
    })
}

Ява

@Override
public void connect() {
    // Picking the first device found on the network.
    WifiP2pDevice device = peers.get(0);

    WifiP2pConfig config = new WifiP2pConfig();
    config.deviceAddress = device.deviceAddress;
    config.wps.setup = WpsInfo.PBC;

    manager.connect(channel, config, new ActionListener() {

        @Override
        public void onSuccess() {
            // WiFiDirectBroadcastReceiver notifies us. Ignore for now.
        }

        @Override
        public void onFailure(int reason) {
            Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
                    Toast.LENGTH_SHORT).show();
        }
    });
}

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

Котлин

manager.requestGroupInfo(channel) { group ->
    val groupPassword = group.passphrase
}

Ява

manager.requestGroupInfo(channel, new GroupInfoListener() {
  @Override
  public void onGroupInfoAvailable(WifiP2pGroup group) {
      String groupPassword = group.getPassphrase();
  }
});

Обратите внимание, что WifiP2pManager.ActionListener , реализованный в методе connect() уведомляет вас только в случае успешного или неудачного запуска . Чтобы прослушивать изменения состояния соединения, реализуйте интерфейс WifiP2pManager.ConnectionInfoListener . Его обратный вызов onConnectionInfoAvailable() уведомляет вас об изменении состояния соединения. В случаях, когда к одному устройству будет подключено несколько устройств (например, игра с тремя или более игроками или приложение для чата), одно устройство назначается «владельцем группы». Вы можете назначить конкретное устройство владельцем группы сети, выполнив действия, описанные в разделе «Создание группы» .

Котлин

private val connectionListener = WifiP2pManager.ConnectionInfoListener { info ->

    // String from WifiP2pInfo struct
    val groupOwnerAddress: String = info.groupOwnerAddress.hostAddress

    // After the group negotiation, we can determine the group owner
    // (server).
    if (info.groupFormed && info.isGroupOwner) {
        // Do whatever tasks are specific to the group owner.
        // One common case is creating a group owner thread and accepting
        // incoming connections.
    } else if (info.groupFormed) {
        // The other device acts as the peer (client). In this case,
        // you'll want to create a peer thread that connects
        // to the group owner.
    }
}

Ява

@Override
public void onConnectionInfoAvailable(final WifiP2pInfo info) {

    // String from WifiP2pInfo struct
    String groupOwnerAddress = info.groupOwnerAddress.getHostAddress();

    // After the group negotiation, we can determine the group owner
    // (server).
    if (info.groupFormed && info.isGroupOwner) {
        // Do whatever tasks are specific to the group owner.
        // One common case is creating a group owner thread and accepting
        // incoming connections.
    } else if (info.groupFormed) {
        // The other device acts as the peer (client). In this case,
        // you'll want to create a peer thread that connects
        // to the group owner.
    }
}

Теперь вернитесь к методу onReceive() приемника широковещательной передачи и измените раздел, который прослушивает намерение WIFI_P2P_CONNECTION_CHANGED_ACTION . Когда это намерение получено, вызовите requestConnectionInfo() . Это асинхронный вызов, поэтому результаты принимаются прослушивателем информации о соединении, который вы указываете в качестве параметра.

Котлин

when (intent.action) {
    ...
    WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {

        // Connection state changed! We should probably do something about
        // that.

        mManager?.let { manager ->

            val networkInfo: NetworkInfo? = intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO) as NetworkInfo

            if (networkInfo?.isConnected == true) {

                // We are connected with the other device, request connection
                // info to find group owner IP

                manager.requestConnectionInfo(channel, connectionListener)
            }
        }
    }
    ...
}

Ява

    ...
    } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

        if (manager == null) {
            return;
        }

        NetworkInfo networkInfo = (NetworkInfo) intent
                .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

        if (networkInfo.isConnected()) {

            // We are connected with the other device, request connection
            // info to find group owner IP

            manager.requestConnectionInfo(channel, connectionListener);
        }
        ...

Создать группу

Если вы хотите, чтобы устройство, на котором запущено ваше приложение, выступало в качестве владельца группы для сети, включающей устаревшие устройства, то есть устройства, не поддерживающие Wi-Fi Direct, выполните ту же последовательность шагов, что и в разделе «Подключение к равноправному узлу». раздел, за исключением того, что вы создаете новый WifiP2pManager.ActionListener , используя createGroup() вместо connect() . Обработка обратного вызова в WifiP2pManager.ActionListener такая же, как показано в следующем фрагменте кода:

Котлин

manager.createGroup(channel, object : WifiP2pManager.ActionListener {
    override fun onSuccess() {
        // Device is ready to accept incoming connections from peers.
    }

    override fun onFailure(reason: Int) {
        Toast.makeText(
                this@WiFiDirectActivity,
                "P2P group creation failed. Retry.",
                Toast.LENGTH_SHORT
        ).show()
    }
})

Ява

manager.createGroup(channel, new WifiP2pManager.ActionListener() {
    @Override
    public void onSuccess() {
        // Device is ready to accept incoming connections from peers.
    }

    @Override
    public void onFailure(int reason) {
        Toast.makeText(WiFiDirectActivity.this, "P2P group creation failed. Retry.",
                Toast.LENGTH_SHORT).show();
    }
});

Примечание. Если все устройства в сети поддерживают Wi-Fi Direct, вы можете использовать метод connect() на каждом устройстве, поскольку этот метод затем создает группу и автоматически выбирает владельца группы.

После создания группы вы можете вызвать requestGroupInfo() чтобы получить подробную информацию об узлах в сети, включая имена устройств и статусы подключения.