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

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

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

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

В этом уроке вы узнаете, как находить близлежащие устройства и подключаться к ним с помощью 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.
    }
});

Keep in mind that this only initiates peer discovery. The discoverPeers() method starts the discovery process and then immediately returns. The system notifies you if the peer discovery process is successfully initiated by calling methods in the provided action listener. Also, discovery remains active until a connection is initiated or a P2P group is formed.

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

Теперь напишите код, который получает и обрабатывает список пиров. Сначала реализуйте интерфейс 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;
        }
    }
}

Now modify your broadcast receiver's onReceive() method to call requestPeers() when an intent with the action WIFI_P2P_PEERS_CHANGED_ACTION is received. You need to pass this listener into the receiver somehow. One way is to send it as an argument to the broadcast receiver's constructor.

Котлин

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

Подключиться к одноранговому узлу

In order to connect to a peer, create a new WifiP2pConfig object, and copy data into it from the WifiP2pDevice representing the device you want to connect to. Then call the connect() method.

Котлин

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

If each of the devices in your group supports Wi-Fi direct, you don't need to explicitly ask for the group's password when connecting. To allow a device that doesn't support Wi-Fi Direct to join a group, however, you need to retrieve this password by calling requestGroupInfo() , as shown in the following code snippet:

Котлин

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() чтобы получить сведения об участниках сети, включая имена устройств и статусы подключения.