Criar conexões P2P com o Wi-Fi Direct

O Wi-Fi Direct (também conhecido como ponto a ponto ou P2P) permite que seu aplicativo encontre dispositivos próximos rapidamente e interaja com eles, com um alcance superior aos recursos do Bluetooth.

As APIs de Wi-Fi ponto a ponto (P2P) permitem que os aplicativos se conectem a dispositivos próximos sem precisar se conectar a uma rede ou ponto de acesso. Caso seu aplicativo tenha sido projetado para fazer parte de uma rede segura e de curto alcance, o Wi-Fi Direct é uma opção mais adequada que a rede Wi-Fi ad-hoc tradicional pelos seguintes motivos:

  • O Wi-Fi Direct é compatível com criptografia WPA2. Algumas redes ad-hoc são compatíveis apenas com criptografia WEP.
  • Os dispositivos podem transmitir os serviços que fornecem, o que ajuda outros dispositivos a descobrirem pontos com mais facilidade.
  • Ao determinar qual dispositivo precisa ser o proprietário do grupo da rede, o Wi-Fi Direct examina os recursos de gerenciamento de energia, IU e serviço de cada dispositivo e usa essas informações para escolher aquele que pode lidar com as responsabilidades do servidor de forma mais eficiente.
  • O Android não é compatível com o modo Wi-Fi ad-hoc.

Esta lição mostra como encontrar dispositivos próximos e conectar-se a eles usando o Wi-Fi P2P.

Configurar permissões de aplicativos

Para usar o Wi-Fi P2P, adicione as permissões ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE, ACCESS_WIFI_STATE e INTERNET ao seu manifesto. O Wi-Fi P2P não requer uma conexão com a Internet, mas usa soquetes Java padrão, que precisam da permissão INTERNET. Portanto, você precisa das seguintes permissões:

    <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_FINE_LOCATION"/>
        <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"/>
        ...
    

Além das permissões anteriores, as seguintes APIs também exigem que o "Modo de localização" esteja ativado:

Configurar um broadcast receiver e um administrador de ponto a ponto

Para usar o Wi-Fi P2P, é preciso detectar intents de transmissão que informam ao seu aplicativo quando determinados eventos ocorreram. No aplicativo, instancie um IntentFilter e configure-o para detectar o seguinte:

WIFI_P2P_STATE_CHANGED_ACTION
Indica se o Wi-Fi P2P está ativado.
WIFI_P2P_PEERS_CHANGED_ACTION
Indica que a lista de pontos disponíveis foi alterada.
WIFI_P2P_CONNECTION_CHANGED_ACTION
Indica que o estado da conectividade do Wi-Fi P2P mudou. A partir do Android 10, essa opção não é fixa. Se seu app dependia do recebimento dessas transmissões no momento de registro porque elas eram fixas, é necessário usar o método get apropriado na inicialização para conseguir as informações.
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
Indica que os detalhes de configuração deste dispositivo foram alterados. A partir do Android 10, essa opção não é fixa. Se seu app dependia do recebimento dessas transmissões no momento de registro porque elas eram fixas, é necessário usar o método get apropriado na inicialização para conseguir as informações.

Kotlin

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

        // Indicates a change in the Wi-Fi P2P 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 P2P 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)
        ...
    }
    

Java

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

No fim do método onCreate(), consiga uma instância do WifiP2pManager e chame o método initialize() dele. Esse método retorna um objeto WifiP2pManager.Channel, que você usará depois para conectar seu app ao framework do Wi-Fi P2P.

Kotlin

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

Java

    Channel channel;
    WifiP2pManager manager;

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

Agora crie uma nova classe BroadcastReceiver, que você usará para detectar as alterações feitas no estado do Wi-Fi P2P do sistema. No método onReceive(), adicione uma condição para processar cada alteração de estado P2P listada acima.

Kotlin

    override fun onReceive(context: Context, intent: Intent) {
        when(intent.action) {
            WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
                // Determine if Wifi P2P 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
                            )
                        }
            }
        }
    }
    

Java

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
            // Determine if Wifi P2P 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));

        }
    }
    

Por fim, adicione um código para registrar o filtro de intent e o broadcast receiver quando sua atividade principal estiver ativa e cancele o registro quando a atividade estiver pausada. O melhor lugar para fazer isso é nos métodos onResume() e onPause().

Kotlin

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

Java

    /** 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);
    }
    

Iniciar descoberta de pontos

Para começar a procurar dispositivos próximos com Wi-Fi P2P, chame discoverPeers(). Esse método tem os seguintes argumentos:

Kotlin

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

Java

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

Lembre-se de que isso apenas inicia a descoberta de pontos. O método discoverPeers() inicia o processo de descoberta e retorna imediatamente. O sistema notifica se o processo de descoberta de pontos foi iniciado com êxito chamando métodos no listener de ação fornecido. Além disso, a descoberta permanece ativa até que uma conexão seja iniciada ou que um grupo P2P seja formado.

Buscar a lista de pontos

Agora, escreva o código que busca e processa a lista de pontos. Primeiro implemente a interface WifiP2pManager.PeerListListener, que fornece informações sobre os pontos que o Wi-Fi P2P detectou. Essas informações também permitem que seu app determine quando os pontos entram ou saem da rede. O seguinte snippet de código ilustra essas operações relacionadas a pontos:

Kotlin

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

Java

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

Agora, modifique o método onReceive() do seu broadcast receiver para chamar requestPeers() quando um intent com a ação WIFI_P2P_PEERS_CHANGED_ACTION for recebida. Você precisa transmitir esse listener ao receiver de alguma forma. Uma maneira é enviá-lo como argumento ao construtor do broadcast receiver.

Kotlin

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

            }
            ...
        }
    }
    

Java

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

Agora, um intent com o intent de ação WIFI_P2P_PEERS_CHANGED_ACTION aciona uma solicitação de uma lista de pontos atualizada.

Conectar-se a um ponto

Para conectar-se a um ponto, crie um novo objeto WifiP2pConfig e copie nele os dados do WifiP2pDevice que representa o dispositivo ao qual você quer se conectar. Depois, chame o método connect().

Kotlin

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

Java

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

Se cada um dos dispositivos do seu grupo for compatível com Wi-Fi Direct, você não precisará pedir explicitamente a senha do grupo ao se conectar. No entanto, para permitir que um dispositivo não compatível com Wi-Fi Direct entre em um grupo, você precisará recuperar essa senha chamando requestGroupInfo(), conforme mostrado no seguinte snippet de código:

Kotlin

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

Java

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

Observe que o WifiP2pManager.ActionListener implementado no método connect() só notifica você quando a inicialização foi concluída ou se falhou. Para detectar alterações no estado da conexão, implemente a interface WifiP2pManager.ConnectionInfoListener. O callback onConnectionInfoAvailable() dela notifica quando o estado da conexão muda. Nos casos em que vários dispositivos serão conectados a um único dispositivo (como um jogo com três ou mais jogadores ou um app de bate-papo), um dispositivo é designado "proprietário do grupo". Você pode designar um dispositivo específico como o proprietário do grupo da rede seguindo as etapas descritas na seção Criar um grupo.

Kotlin

    private val connectionListener = WifiP2pManager.ConnectionInfoListener { info ->

        // InetAddress 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.
        }
    }
    

Java

    @Override
    public void onConnectionInfoAvailable(final WifiP2pInfo info) {

        // InetAddress from WifiP2pInfo struct.
        InetAddress 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.
        }
    }
    

Agora, volte ao método onReceive() do broadcast receiver e modifique a seção que detecta um intent WIFI_P2P_CONNECTION_CHANGED_ACTION. Quando esse intent for recebido, chame requestConnectionInfo(). Como essa é uma chamada assíncrona, os resultados são recebidos pelo listener de informações da conexão que você fornece como parâmetro.

Kotlin

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

Java

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

Criar um grupo

Para que o dispositivo que está executando seu app seja o proprietário do grupo de uma rede com dispositivos legados, ou seja, não compatíveis com Wi-Fi Direct, siga a mesma sequência de etapas da seção Conectar-se a um ponto, a menos que você crie um novo WifiP2pManager.ActionListener usando createGroup(), em vez de connect(). O callback que é processado no WifiP2pManager.ActionListener é o mesmo, conforme mostrado no seguinte snippet de código:

Kotlin

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

Java

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

Observação: se todos os dispositivos de uma rede forem compatíveis com Wi-Fi Direct, você poderá usar o método connect() em cada um deles, porque esse método cria o grupo e seleciona um proprietário de grupo automaticamente.

Depois de criar um grupo, você pode chamar requestGroupInfo() para recuperar detalhes sobre os pontos da rede, incluindo nomes de dispositivos e status de conexões.