Wi-Fi directo (P2P) permite que dispositivos Android 4.0 (API nivel 14) o versiones posteriores con el hardware adecuado se conecten directamente entre sí mediante Wi-Fi sin un punto de acceso intermedio. A través de estas API, es posible detectar otros dispositivos y conectarse a ellos si cada uno admite P2P Wi-Fi, y después comunicarse mediante una conexión rápida a distancias mayores que las permitidas por una conexión Bluetooth. Esto es útil para las aplicaciones que comparten datos entre usuarios, como un juego multijugador o una aplicación de intercambio de fotos.
Las API P2P Wi-Fi constan de las siguientes partes principales:
Los métodos que permiten detectar pares, solicitarlos y conectarse a ellos se definen en la clase
WifiP2pManager
.Los receptores que permiten recibir la notificación respecto de si una llamada de método
WifiP2pManager
se realizó correctamente o no. Al llamar a métodosWifiP2pManager
, cada método puede recibir un receptor específico que pasa como un parámetro.Los intents que notifican acerca de eventos específicos detectados por el marco de trabajo de P2P Wi-Fi, por ejemplo, una conexión perdida o un par recién detectado.
A menudo, estos tres componentes de las API se utilizan juntos. Por ejemplo, puedes proporcionar WifiP2pManager.ActionListener
para una llamada a discoverPeers()
de modo que se te pueda notificar con los métodos ActionListener.onSuccess()
y ActionListener.onFailure()
. Un intent WIFI_P2P_PEERS_CHANGED_ACTION
también se transmite si el método discoverPeers()
detecta que la lista de pares se modificó.
Descripción general de la API
La clase WifiP2pManager
proporciona métodos que te permiten interactuar con el hardware Wi-Fi de tu dispositivo o, por ejemplo, detectar pares y conectarte con ellos. Las siguientes acciones están disponibles:
Tabla 1.Métodos P2P Wi-Fi
Método | Descripción |
---|---|
initialize() |
Registra la aplicación con el marco de trabajo Wi-Fi. Debe llamarse a este método antes de llamar a cualquier otro método P2P Wi-Fi. |
connect() |
Inicia una conexión entre pares con un dispositivo que tiene la configuración especificada. |
cancelConnect() |
Cancela cualquier negociación de grupo entre pares que haya en curso. |
requestConnectInfo() |
Solicita la información de conexión de un dispositivo. |
createGroup() |
Crea un grupo entre pares con el dispositivo actual como propietario del grupo. |
removeGroup() |
Elimina el grupo entre pares actual. |
requestGroupInfo() |
Solicita información del grupo entre pares. |
discoverPeers() |
Inicia la detección de pares. |
requestPeers() |
Solicita la lista actual de pares detectados. |
Los métodos WifiP2pManager
permiten pasar un receptor de modo que el marco de trabajo P2P Wi-Fi pueda notificar en tu actividad el estado de una llamada. Las interfaces de receptor disponibles y las llamadas al método WifiP2pManager
correspondientes que usan los receptores se describen en la siguiente tabla:
Tabla 2: Receptores de P2P Wi-Fi
Las API P2P de Wi-Fi definen los intents que se transmiten cuando ocurren determinados eventos de P2P Wi-Fi, por ejemplo, cuando se detecta un nuevo par o se modifica el estado de Wi-Fi de un dispositivo. Si deseas registrar la recepción de estos intents en la aplicación, puedes crear un receptor de emisión que maneje estos intents:
Tabla 3: Intents de P2P Wi-Fi
Intent | Descripción |
---|---|
WIFI_P2P_CONNECTION_CHANGED_ACTION |
Transmite cuando se modifica el estado de la conexión Wi-Fi del dispositivo. |
WIFI_P2P_PEERS_CHANGED_ACTION |
Transmite cuando llamas a discoverPeers() . A menudo, se llama a requestPeers() para ver una lista actualizada de pares si manejas este intent en tu aplicación. |
WIFI_P2P_STATE_CHANGED_ACTION |
Transmite cuando P2P Wi-Fi se habilita o se inhabilita en el dispositivo. |
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION |
Transmite cuando se modifican los detalles de un dispositivo, por ejemplo, el nombre del dispositivo. |
Crear un receptor de emisión para intents P2P Wi-Fi
Un receptor de emisión permite recibir los intents que transmite el sistema Android de modo que la aplicación pueda responder a los eventos que te interesan. Los pasos básicos para crear un receptor de emisión a fin de manejar los intents P2P Wi-Fi son los siguientes:
Crea una clase que extienda la clase
BroadcastReceiver
. Para el constructor de la clase, seguramente, querrás contar con los parámetros deWifiP2pManager
,WifiP2pManager.Channel
y la actividad en la que se registrará este receptor de emisión. De este modo, el receptor de emisión puede enviar actualizaciones a la actividad, así como contar con acceso al hardware Wi-Fi y un canal de comunicación si es necesario.En el receptor de emisión, busca los intents que te interesan del método
onReceive()
. Realiza las acciones necesarias en función del intent recibido. Por ejemplo, si el receptor de emisión recibe un intent deWIFI_P2P_PEERS_CHANGED_ACTION
, puedes llamar al métodorequestPeers()
para ver una lista de los pares detectados actualmente.
El siguiente código muestra cómo crear un receptor de emisión típico. El receptor de emisión toma un objeto WifiP2pManager
y una actividad como argumentos, y utiliza estas dos clases para realizar correctamente las acciones necesarias cuando el receptor de emisión recibe un intent:
Kotlin
/** * A BroadcastReceiver that notifies of important Wi-Fi p2p events. */ class WiFiDirectBroadcastReceiver( private val manager: WifiP2pManager, private val channel: WifiP2pManager.Channel, private val activity: MyWifiActivity ) : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action: String = intent.action when (action) { WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> { // Check to see if Wi-Fi is enabled and notify appropriate activity } WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> { // Call WifiP2pManager.requestPeers() to get a list of current peers } WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { // Respond to new connection or disconnections } WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> { // Respond to this device's wifi state changing } } } }
Java
/** * A BroadcastReceiver that notifies of important Wi-Fi p2p events. */ public class WiFiDirectBroadcastReceiver extends BroadcastReceiver { private WifiP2pManager mManager; private Channel mChannel; private MyWiFiActivity mActivity; public WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel channel, MyWifiActivity activity) { super(); this.mManager = manager; this.mChannel = channel; this.mActivity = activity; } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) { // Check to see if Wi-Fi is enabled and notify appropriate activity } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) { // Call WifiP2pManager.requestPeers() to get a list of current peers } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) { // Respond to new connection or disconnections } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) { // Respond to this device's wifi state changing } } }
Con Android Q, los siguientes intents de transmisión se modificaron de fijos a no fijos:
WIFI_P2P_CONNECTION_CHANGED_ACTION
- Las aplicaciones pueden usar
requestConnectionInfo()
,requestNetworkInfo()
orequestGroupInfo()
para recuperar la información de conexión actual. WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
- Las aplicaciones pueden usar
requestDeviceInfo()
para recuperar la información de conexión actual.
Crear una aplicación P2P Wi-Fi
La creación de una aplicación P2P Wi-Fi implica desarrollar y registrar un receptor de emisión para la aplicación, detectar pares, conectarse a un par y transferir datos a un par. En las siguientes secciones, se describe cómo hacerlo.
Configuración inicial
Antes de usar las API de P2P Wi-Fi, debes asegurarte de que la aplicación pueda acceder al hardware y de que el dispositivo admita el protocolo P2P Wi-Fi. Si lo admite, puedes obtener una instancia de WifiP2pManager
, crear y registrar el receptor de emisión, y comenzar a usar las API de P2P Wi-Fi.
Solicita permiso para usar el hardware Wi-Fi en el dispositivo y, asimismo, declara que tu aplicación cuenta con la versión mínima de SDK correcta en el manifiesto de Android:
<uses-sdk android:minSdkVersion="14" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Además de los permisos mencionados, las siguientes API también requieren que se habilite el Modo de ubicación:
Verifica que P2P Wi-Fi esté activado y sea compatible. Un buen lugar para verificarlo es el receptor de emisión cuando recibe el intent
WIFI_P2P_STATE_CHANGED_ACTION
. Notifica en tu actividad el estado de P2P Wi-Fi y actúa en consecuencia:Kotlin
override fun onReceive(context: Context, intent: Intent) { ... val action: String = intent.action when (action) { WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> { val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1) when (state) { WifiP2pManager.WIFI_P2P_STATE_ENABLED -> { // Wifi P2P is enabled } else -> { // Wi-Fi P2P is not enabled } } } } ... }
Java
@Override public void onReceive(Context context, Intent intent) { ... String action = intent.getAction(); if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) { int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1); if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) { // Wifi P2P is enabled } else { // Wi-Fi P2P is not enabled } } ... }
En el método
onCreate()
de tu actividad, puedes obtener una instancia deWifiP2pManager
y registrar la aplicación con el marco de trabajo de P2P Wi-Fi llamando ainitialize()
. Este método devuelve el resultadoWifiP2pManager.Channel
, que se usa para conectar la aplicación al marco de trabajo de P2P Wi-Fi. Asimismo, debes crear una instancia del receptor de emisión con los objetosWifiP2pManager
yWifiP2pManager.Channel
junto con una referencia a la actividad. De este modo, el receptor de emisión podrá notificar en tu actividad si hay eventos interesantes y actualizarla en consecuencia. También te permitirá manipular el estado de Wi-Fi si es necesario:Kotlin
val manager: WifiP2pManager? by lazy(LazyThreadSafetyMode.NONE) { getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager? } var mChannel: WifiP2pManager.Channel? = null var receiver: BroadcastReceiver? = null override fun onCreate(savedInstanceState: Bundle?) { ... mChannel = manager?.initialize(this, mainLooper, null) mChannel?.also { channel -> receiver = WiFiDirectBroadcastReceiver(manager, channel, this) } }
Java
WifiP2pManager manager; Channel channel; BroadcastReceiver receiver; ... @Override protected void onCreate(Bundle savedInstanceState){ ... manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE); channel = manager.initialize(this, getMainLooper(), null); receiver = new WiFiDirectBroadcastReceiver(manager, mChannel, this); ... }
Crea un filtro de intents y agrega los mismos intents que busca el receptor de emisión:
Kotlin
val intentFilter = IntentFilter().apply { addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION) }
Java
IntentFilter intentFilter; ... @Override protected void onCreate(Bundle savedInstanceState){ ... intentFilter = new IntentFilter(); intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); ... }
Registra el receptor de emisión en el método
onResume()
de tu actividad y cancela el registro en el métodoonPause()
de tu actividad:Kotlin
/* register the broadcast receiver with the intent values to be matched */ override fun onResume() { super.onResume() mReceiver?.also { receiver -> registerReceiver(receiver, intentFilter) } } /* unregister the broadcast receiver */ override fun onPause() { super.onPause() mReceiver?.also { receiver -> unregisterReceiver(receiver) } }
Java
/* register the broadcast receiver with the intent values to be matched */ @Override protected void onResume() { super.onResume(); registerReceiver(mReceiver, intentFilter); } /* unregister the broadcast receiver */ @Override protected void onPause() { super.onPause(); unregisterReceiver(mReceiver); }
Cuando hayas obtenido un
WifiP2pManager.Channel
y configurado un receptor de emisión, la aplicación podrá hacer llamadas al método P2P Wi-Fi y recibir intents de P2P Wi-Fi.Ahora puedes implementar la aplicación y usar las funciones de P2P Wi-Fi; para ello, llama a los métodos en
WifiP2pManager
. En las siguientes secciones, se describe cómo realizar acciones comunes, por ejemplo, detectar pares y conectarse a ellos.
Cómo detectar pares
Para detectar pares a los que puedes conectarte, llama a discoverPeers()
a fin de detectar los pares disponibles en el rango. La llamada a esta función es asíncrona y se comunica un mensaje de éxito o falla a la aplicación con onSuccess()
y onFailure()
si creaste un WifiP2pManager.ActionListener
. El método onSuccess()
solamente te notifica de que el proceso de detección fue exitoso y no proporciona información alguna sobre los pares que efectivamente detectó (si detectó alguno):
Kotlin
manager?.discoverPeers(channel, object : WifiP2pManager.ActionListener { override fun onSuccess() { ... } override fun onFailure(reasonCode: Int) { ... } })
Java
manager.discoverPeers(channel, new WifiP2pManager.ActionListener() { @Override public void onSuccess() { ... } @Override public void onFailure(int reasonCode) { ... } });
Si el proceso de detección se realiza con éxito y detecta pares, el sistema transmite el intent WIFI_P2P_PEERS_CHANGED_ACTION
, que puedes recibir en un receptor de emisión para conocer la lista de pares. Cuando la aplicación recibe el intent WIFI_P2P_PEERS_CHANGED_ACTION
, puedes solicitar una lista de los pares detectados con requestPeers()
. El siguiente código muestra cómo configurarlo:
Kotlin
override fun onReceive(context: Context, intent: Intent) { val action: String = intent.action when (action) { ... WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> { manager?.requestPeers(channel) { peers: WifiP2pDeviceList? -> // Handle peers list } } ... } }
Java
PeerListListener myPeerListListener; ... 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 (manager != null) { manager.requestPeers(channel, myPeerListListener); } }
El método requestPeers()
también es asíncrono y puede notificar en tu actividad que hay una lista de pares disponible en onPeersAvailable()
, lo cual se define en la interfaz de WifiP2pManager.PeerListListener
. El método onPeersAvailable()
te proporciona un WifiP2pDeviceList
, que puedes iterar para buscar el par al que deseas conectarte.
Cómo conectarse a pares
Cuando hayas decidido a qué dispositivo quieres conectarte después de obtener una lista de posibles pares, llama al método connect()
para conectarte al dispositivo. Esta llamada de método requiere un objeto WifiP2pConfig
que contenga la información del dispositivo al que vas a conectarte. Se te puede notificar del éxito o de la falla de una conexión mediante WifiP2pManager.ActionListener
. El siguiente código muestra cómo crear una conexión con el dispositivo que elijas:
Kotlin
val device: WifiP2pDevice = ... val config = WifiP2pConfig() config.deviceAddress = device.deviceAddress mChannel?.also { channel -> manager?.connect(channel, config, object : WifiP2pManager.ActionListener { override fun onSuccess() { //success logic } override fun onFailure(reason: Int) { //failure logic } } })
Java
//obtain a peer from the WifiP2pDeviceList WifiP2pDevice device; WifiP2pConfig config = new WifiP2pConfig(); config.deviceAddress = device.deviceAddress; manager.connect(channel, config, new ActionListener() { @Override public void onSuccess() { //success logic } @Override public void onFailure(int reason) { //failure logic } });
Transferir datos
Una vez establecida una conexión, puedes transferir datos entre los dispositivos con sockets. A continuación, se muestran los pasos básicos para transferir datos:
Crea un
ServerSocket
. Este socket espera una conexión de un cliente en un puerto especificado y se bloquea hasta que la conexión ocurre; por lo tanto, debes realizar esta tarea en un subproceso en segundo plano.Crea un cliente
Socket
. El cliente utiliza la dirección IP y el puerto del socket de servidor para conectarse al dispositivo del servidor.Envía datos desde el cliente al servidor. Cuando el socket de cliente se conecta correctamente con el socket de servidor, puedes enviar datos desde el cliente al servidor con flujo de bytes.
El socket de servidor espera una conexión del cliente (con el método
accept()
). La llamada se bloquea hasta que un cliente se conecta; por lo tanto, debes realizar la llamada en otro subproceso. Cuando ocurre una conexión, el dispositivo del servidor puede recibir los datos del cliente. Realiza alguna acción con estos datos, por ejemplo, guárdalos en un archivo o preséntaselos al usuario.
En el siguiente ejemplo, modificado a partir de la Demostración de P2P Wi-Fi, se muestra cómo crear esta comunicación de socket entre cliente y servidor, y cómo transferir imágenes JPEG desde un cliente hacia un servidor mediante un servicio. Para ver un ejemplo de funcionamiento completo, compila y ejecuta la Demostración de P2P Wi-Fi.
Kotlin
class FileServerAsyncTask( private val context: Context, private var statusText: TextView ) : AsyncTask<Void, Void, String?>() { override fun doInBackground(vararg params: Void): String? { /** * Create a server socket. */ val serverSocket = ServerSocket(8888) return serverSocket.use { /** * Wait for client connections. This call blocks until a * connection is accepted from a client. */ val client = serverSocket.accept() /** * If this code is reached, a client has connected and transferred data * Save the input stream from the client as a JPEG file */ val f = File(Environment.getExternalStorageDirectory().absolutePath + "/${context.packageName}/wifip2pshared-${System.currentTimeMillis()}.jpg") val dirs = File(f.parent) dirs.takeIf { it.doesNotExist() }?.apply { mkdirs() } f.createNewFile() val inputstream = client.getInputStream() copyFile(inputstream, FileOutputStream(f)) serverSocket.close() f.absolutePath } } private fun File.doesNotExist(): Boolean = !exists() /** * Start activity that can handle the JPEG image */ override fun onPostExecute(result: String?) { result?.run { statusText.text = "File copied - $result" val intent = Intent(android.content.Intent.ACTION_VIEW).apply { setDataAndType(Uri.parse("file://$result"), "image/*") } context.startActivity(intent) } } }
Java
public static class FileServerAsyncTask extends AsyncTask { private Context context; private TextView statusText; public FileServerAsyncTask(Context context, View statusText) { this.context = context; this.statusText = (TextView) statusText; } @Override protected String doInBackground(Void... params) { try { /** * Create a server socket and wait for client connections. This * call blocks until a connection is accepted from a client */ ServerSocket serverSocket = new ServerSocket(8888); Socket client = serverSocket.accept(); /** * If this code is reached, a client has connected and transferred data * Save the input stream from the client as a JPEG file */ final File f = new File(Environment.getExternalStorageDirectory() + "/" + context.getPackageName() + "/wifip2pshared-" + System.currentTimeMillis() + ".jpg"); File dirs = new File(f.getParent()); if (!dirs.exists()) dirs.mkdirs(); f.createNewFile(); InputStream inputstream = client.getInputStream(); copyFile(inputstream, new FileOutputStream(f)); serverSocket.close(); return f.getAbsolutePath(); } catch (IOException e) { Log.e(WiFiDirectActivity.TAG, e.getMessage()); return null; } } /** * Start activity that can handle the JPEG image */ @Override protected void onPostExecute(String result) { if (result != null) { statusText.setText("File copied - " + result); Intent intent = new Intent(); intent.setAction(android.content.Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse("file://" + result), "image/*"); context.startActivity(intent); } } }
En el cliente, conéctate al socket de servidor con un socket de cliente y transfiere los datos. En este ejemplo, se transfiere un archivo JPEG en el sistema de archivos del dispositivo de cliente.
Kotlin
val context = applicationContext val host: String val port: Int val len: Int val socket = Socket() val buf = ByteArray(1024) ... try { /** * Create a client socket with the host, * port, and timeout information. */ socket.bind(null) socket.connect((InetSocketAddress(host, port)), 500) /** * Create a byte stream from a JPEG file and pipe it to the output stream * of the socket. This data is retrieved by the server device. */ val outputStream = socket.getOutputStream() val cr = context.contentResolver val inputStream: InputStream = cr.openInputStream(Uri.parse("path/to/picture.jpg")) while (inputStream.read(buf).also { len = it } != -1) { outputStream.write(buf, 0, len) } outputStream.close() inputStream.close() } catch (e: FileNotFoundException) { //catch logic } catch (e: IOException) { //catch logic } finally { /** * Clean up any open sockets when done * transferring or if an exception occurred. */ socket.takeIf { it.isConnected }?.apply { close() } }
Java
Context context = this.getApplicationContext(); String host; int port; int len; Socket socket = new Socket(); byte buf[] = new byte[1024]; ... try { /** * Create a client socket with the host, * port, and timeout information. */ socket.bind(null); socket.connect((new InetSocketAddress(host, port)), 500); /** * Create a byte stream from a JPEG file and pipe it to the output stream * of the socket. This data is retrieved by the server device. */ OutputStream outputStream = socket.getOutputStream(); ContentResolver cr = context.getContentResolver(); InputStream inputStream = null; inputStream = cr.openInputStream(Uri.parse("path/to/picture.jpg")); while ((len = inputStream.read(buf)) != -1) { outputStream.write(buf, 0, len); } outputStream.close(); inputStream.close(); } catch (FileNotFoundException e) { //catch logic } catch (IOException e) { //catch logic } /** * Clean up any open sockets when done * transferring or if an exception occurred. */ finally { if (socket != null) { if (socket.isConnected()) { try { socket.close(); } catch (IOException e) { //catch logic } } } }