Descripción general del reconocimiento de Wi-Fi

Las funciones de reconocimiento de Wi-Fi permiten que los dispositivos con Android 8.0 (API nivel 26) y versiones posteriores se descubran y se conecten directamente unos con otros sin ningún otro tipo de conectividad entre ellos. El reconocimiento de Wi-Fi también se conoce como Neighbour Awareness Networking (NAN).

La red de reconocimiento de Wi-Fi funciona formando clústeres con dispositivos cercanos o creando un clúster nuevo si el dispositivo es el primero en un área. Este comportamiento de agrupamiento en clústeres se aplica a todo el dispositivo y se administra mediante el servicio del sistema de reconocimiento de Wi-Fi; las apps no tienen control sobre este comportamiento. Las apps usan las API de reconocimiento de Wi-Fi para comunicarse con ese servicio del sistema, que administra el hardware para tal fin en el dispositivo.

Las API de reconocimiento de Wi-Fi permiten que las apps realicen las siguientes operaciones:

  • Descubrir otros dispositivos: La API tiene un mecanismo para encontrar otros dispositivos cercanos. El proceso comienza cuando un dispositivo publica uno o más servicios detectables. Luego, cuando un dispositivo se suscribe a uno o más servicios y entra en el rango de Wi-Fi del publicador, el suscriptor recibe una notificación de que se descubrió un publicador coincidente. Después de que el suscriptor descubre un publicador, puede enviar un mensaje corto o establecer una conexión de red con el dispositivo descubierto. Los dispositivos pueden ser simultáneamente publicadores y suscriptores.

  • Crear una conexión de red: Después de que dos dispositivos se hayan descubierto, podrán crear una conexión de red de reconocimiento de Wi-Fi bidireccional sin un punto de acceso.

Las conexiones de red de reconocimiento de Wi-Fi admiten mayores tasas de capacidad de procesamiento en distancias más largas que las conexiones Bluetooth. Estos tipos de conexiones son útiles para apps que comparten grandes cantidades de datos entre usuarios, como las apps que permiten compartir fotos.

Configuración inicial

Si quieres configurar tu app para usar el descubrimiento y las redes de reconocimiento de Wi-Fi, realiza los siguientes pasos:

  1. Solicita los siguientes permisos en el manifiesto de tu app:

        <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" />
        
  2. Comprueba si el dispositivo admite el reconocimiento de Wi-Fi con la API PackageManager, como se muestra a continuación:

    Kotlin

        context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)
        

    Java

        context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE);
        
  3. Comprueba si el reconocimiento de Wi-Fi está disponible actualmente. Es posible que el reconocimiento de Wi-Fi exista en el dispositivo, pero quizás no esté disponible porque el usuario inhabilitó la conexión Wi-Fi o la ubicación. Según sus capacidades de hardware y firmware, es posible que algunos dispositivos no admitan el reconocimiento de Wi-Fi si Wi-Fi directo, SoftAP o la conexión mediante dispositivo móvil están en uso. Para verificar si el reconocimiento de Wi-Fi está disponible actualmente, llama a isAvailable().

    La disponibilidad del reconocimiento de Wi-Fi puede cambiar en cualquier momento. Tu app debe registrar un BroadcastReceiver para recibir ACTION_WIFI_AWARE_STATE_CHANGED, que se envía cuando la disponibilidad cambia. Cuando tu app recibe el intent de emisión, debe descartar todas las sesiones existentes (supongamos que el servicio de reconocimiento de Wi-Fi se interrumpió); luego, verifica el estado actual de disponibilidad y ajusta su comportamiento en consecuencia. Por ejemplo:

    Kotlin

        val wifiAwareManager = context.getSystemService(Context.WIFI_AWARE_SERVICE) as WifiAwareManager?
        val filter = IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED)
        val myReceiver = object : BroadcastReceiver() {
    
            override fun onReceive(context: Context, intent: Intent) {
                // discard current sessions
                if (wifiAwareManager?.isAvailable) {
                    ...
                } else {
                    ...
                }
            }
        }
        context.registerReceiver(myReceiver, filter)
        

    Java

        WifiAwareManager wifiAwareManager =
                (WifiAwareManager)context.getSystemService(Context.WIFI_AWARE_SERVICE)
        IntentFilter filter =
                new IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED);
        BroadcastReceiver myReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // discard current sessions
                if (wifiAwareManager.isAvailable()) {
                    ...
                } else {
                    ...
                }
            }
        };
        context.registerReceiver(myReceiver, filter);
        

Para obtener más información, consulta Transmisiones.

Obtén una sesión

Para empezar a usar el reconocimiento de Wi-Fi, tu app debe obtener un WifiAwareSession llamando a attach(). Este método hace lo siguiente:

  • Activa el hardware de reconocimiento de Wi-Fi.
  • Se une a un clúster de reconocimiento de Wi-Fi o forma uno.
  • Crea una sesión de reconocimiento de Wi-Fi con un espacio de nombres único que actúa como un contenedor para todas las sesiones de descubrimiento creadas en él.

Si la app se conecta correctamente, el sistema ejecuta la devolución de llamada onAttached(). Esta devolución de llamada proporciona un objeto WifiAwareSession que tu app debe usar para todas las operaciones de sesión adicionales. Una app puede usar la sesión para publicar un servicio o suscribirse a un servicio.

Tu app debe llamar a attach() solo una vez. Si tu app llama a attach() varias veces, recibe una sesión diferente para cada llamada, cada una con su propio espacio de nombres. Esto podría ser útil en escenarios complejos, pero por lo general debe evitarse.

Publica un servicio

Para hacer que un servicio sea detectable, llama al método publish(), que toma los siguientes parámetros:

  • PublishConfig especifica el nombre del servicio y otras propiedades de configuración, como el filtro de coincidencias.
  • DiscoverySessionCallback especifica las acciones que se deben ejecutar cuando se producen eventos, como cuando el suscriptor recibe un mensaje.

Por ejemplo:

Kotlin

    val config: PublishConfig = PublishConfig.Builder()
            .setServiceName(AWARE_FILE_SHARE_SERVICE_NAME)
            .build()
    awareSession.publish(config, object : DiscoverySessionCallback() {

        override fun onPublishStarted(session: PublishDiscoverySession) {
            ...
        }

        override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
            ...
        }
    })
    

Java

    PublishConfig config = new PublishConfig.Builder()
        .setServiceName(“Aware_File_Share_Service_Name”)
        .build();

    awareSession.publish(config, new DiscoverySessionCallback() {
        @Override
        public void onPublishStarted(PublishDiscoverySession session) {
            ...
        }
        @Override
        public void onMessageReceived(PeerHandle peerHandle, byte[] message) {
            ...
        }
    }, null);

    

Si la publicación se realiza correctamente, se llama al método de devolución de llamada onPublishStarted().

Después de la publicación, cuando los dispositivos que ejecutan apps de suscriptor coincidentes pasan al rango de Wi-Fi del dispositivo de publicación, los suscriptores descubren el servicio. Cuando un suscriptor descubre un publicador, el publicador no recibe una notificación; sin embargo, si el suscriptor envía un mensaje al publicador, el publicador recibe una notificación. Cuando eso sucede, se llama al método de devolución de llamada onMessageReceived(). Puedes usar el argumento PeerHandle de este método para enviar un mensaje de vuelta al suscriptor o crear una conexión con él.

Para dejar de publicar el servicio, llama a DiscoverySession.close(). Las sesiones de descubrimiento están asociadas con su WifiAwareSession principal. Si la sesión principal está cerrada, sus sesiones de descubrimiento asociadas también están cerradas. Si bien los objetos descartados también están cerrados, el sistema no garantiza cuándo se cierran las sesiones fuera del alcance, por lo que recomendamos que llames explícitamente a los métodos close().

Suscríbete a un servicio

Para suscribirte a un servicio, llama al método subscribe(), que toma los siguientes parámetros:

  • SubscribeConfig especifica el nombre del servicio al cual suscribirse y otras propiedades de configuración, como el filtro de coincidencias.
  • DiscoverySessionCallback especifica las acciones que se ejecutarán cuando se produzcan eventos, como cuando se descubre un publicador.

Por ejemplo:

Kotlin

    val config: SubscribeConfig = SubscribeConfig.Builder()
            .setServiceName(AWARE_FILE_SHARE_SERVICE_NAME)
            .build()
    awareSession.subscribe(config, object : DiscoverySessionCallback() {

        override fun onSubscribeStarted(session: SubscribeDiscoverySession) {
            ...
        }

        override fun onServiceDiscovered(
                peerHandle: PeerHandle,
                serviceSpecificInfo: ByteArray,
                matchFilter: List<ByteArray>
        ) {
            ...
        }
    }, null)
    

Java

    SubscribeConfig config = new SubscribeConfig.Builder()
        .setServiceName("Aware_File_Share_Service_Name")
        .build();

    awareSession.subscribe(config, new DiscoverySessionCallback() {
        @Override
        public void onSubscribeStarted(SubscribeDiscoverySession session) {
            ...
        }

        @Override
        public void onServiceDiscovered(PeerHandle peerHandle,
                byte[] serviceSpecificInfo, List<byte[]> matchFilter) {
            ...
        }
    }, null);

    

Si la operación de suscripción se realiza correctamente, el sistema llama a la devolución de llamada onSubscribeStarted() en tu app. Como puedes usar el argumento SubscribeDiscoverySession en la devolución de llamada para comunicarte con un publicador después de que tu app haya descubierto uno, debes guardar esta referencia. Puedes actualizar la sesión de suscripción en cualquier momento llamando a updateSubscribe() en la sesión de descubrimiento.

En este momento, tu suscripción esperará a que los publicadores coincidan para entrar en el rango de Wi-Fi. Cuando esto sucede, el sistema ejecuta el método de devolución de llamada onServiceDiscovered(). Puedes usar el argumento PeerHandle de esta devolución de llamada para enviar un mensaje o crear una conexión con ese publicador.

Para cancelar la suscripción a un servicio, llama a DiscoverySession.close(). Las sesiones de descubrimiento están asociadas con su WifiAwareSession principal. Si la sesión principal está cerrada, sus sesiones de descubrimiento asociadas también están cerradas. Si bien los objetos descartados también están cerrados, el sistema no garantiza cuándo se cierran las sesiones fuera del alcance, por lo que recomendamos que llames explícitamente a los métodos close().

Envía un mensaje

Para enviar un mensaje a otro dispositivo, necesitas los siguientes objetos:

Para enviar un mensaje, llama a sendMessage(). A continuación, pueden ocurrir las siguientes devoluciones de llamada:

  • Cuando la app similar recibe correctamente el mensaje, el sistema llama a la devolución de llamada onMessageSendSucceeded() en la app emisora.
  • Cuando la app similar recibe un mensaje, el sistema llama a la devolución de llamada onMessageReceived() en la app receptora.

Aunque se requiere PeerHandle para comunicarse con las apps similares, no debes confiar en él como un identificador permanente de apps similares. La aplicación puede usar identificadores de nivel superior, incorporados en el servicio de descubrimiento o en los mensajes posteriores. Puede incorporar un identificador en el servicio de descubrimiento con el método setMatchFilter() o setServiceSpecificInfo() de PublishConfig o SubscribeConfig. El método setMatchFilter() afecta el descubrimiento, mientras que el método setServiceSpecificInfo() no lo hace.

Incorporar un identificador en un mensaje implica modificar el arreglo de bytes del mensaje para incluir un identificador (por ejemplo, como el primer par de bytes).

Crea una conexión

El reconocimiento de Wi-Fi admite la red cliente-servidor entre dos dispositivos con esa función.

Para configurar la conexión cliente-servidor:

  1. Usa el descubrimiento de reconocimiento de Wi-Fi para publicar un servicio (en el servidor) y suscribirte a un servicio (en el cliente).

  2. Cuando el suscriptor descubre al publicador, le envía un mensaje.

  3. Inicia un ServerSocket en el dispositivo del publicador y configura u obtén su puerto:

    Kotlin

        val ss = ServerSocket(0)
        val port = ss.localPort
        

    Java

        ServerSocket ss = new ServerSocket(0);
        int port = ss.getLocalPort();
        
  4. Usa el ConnectivityManager para solicitar una red de reconocimiento de Wi-Fi en el publicador con un WifiAwareNetworkSpecifier y especifica la sesión de descubrimiento y el PeerHandle del suscriptor, que obtuviste del mensaje que transmitió el suscriptor:

    Kotlin

        val networkSpecifier = WifiAwareNetworkSpecifier.Builder(discoverySession, peerHandle)
            .setPskPassphrase("somePassword")
            .setPort(port)
            .build()
        val myNetworkRequest = NetworkRequest.Builder()
            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
            .setNetworkSpecifier(networkSpecifier)
            .build()
        val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                ...
            }
    
            override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
                ...
            }
    
            override fun onLost(network: Network) {
                ...
            }
        }
    
        connMgr.requestNetwork(networkRequest, callback);
        

    Java

        NetworkSpecifier networkSpecifier = new WifiAwareNetworkSpecifier.Builder(discoverySession, peerHandle)
            .setPskPassphrase("somePassword")
            .setPort(port)
            .build();
        NetworkRequest myNetworkRequest = new NetworkRequest.Builder()
            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
            .setNetworkSpecifier(networkSpecifier)
            .build();
        ConnectivityManager.NetworkCallback callback = new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(Network network) {
                ...
            }
    
            @Override
            public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
                ...
            }
    
            @Override
            public void onLost(Network network) {
                ...
            }
        };
    
        ConnectivityManager connMgr.requestNetwork(networkRequest, callback);
        
  5. Solicita una red de reconocimiento de Wi-Fi en el suscriptor con el mismo método que en el publicador. No especifiques un puerto cuando crees el NetworkSpecifier. Se llama a los métodos de devolución de llamada adecuados cuando la conexión de red está disponible, cambia o se pierde.

  6. Cuando se llama al método onAvailable() en el suscriptor, un objeto Network está disponible, y con él puedes abrir un Socket para comunicarte con el ServerSocket en el publicador, pero necesitas conocer la dirección iPv6 y el puerto de ServerSocket. Los obtienes del objeto NetworkCapabilities provisto en la devolución de llamada onCapabilitiesChanged():

    Kotlin

        val peerAwareInfo = networkCapabilities.transportInfo as WifiAwareNetworkInfo
        val peerIpv6 = peerAwareInfo.peerIpv6Addr
        val peerPort = peerAwareInfo.port
        ...
        val socket = network.getSocketFactory().createSocket(peerIpv6, peerPort)
        

    Java

        WifiAwareNetworkInfo peerAwareInfo = (WifiAwareNetworkInfo) networkCapabilities.getTransportInfo();
        Inet6Address peerIpv6 = peerAwareInfo.getPeerIpv6Addr();
        int peerPort = peerAwareInfo.getPort();
        ...
        Socket socket = network.getSocketFactory().createSocket(peerIpv6, peerPort);
        
  7. Cuando termines con la conexión de red, llama a unregisterNetworkCallback().

Rango de app similares y descubrimiento del conocimiento de la ubicación

Un dispositivo con capacidades de ubicación de Wi-Fi RTT puede medir directamente la distancia a apps similares y usar esta información para restringir el descubrimiento del servicio de reconocimiento de Wi-Fi.

La API de Wi-Fi RTT permite el rango directo a una app similar con reconocimiento de Wi-Fi usando su dirección MAC o su PeerHandle.

El descubrimiento de reconocimiento de Wi-Fi se puede restringir para descubrir solo servicios dentro de un geovallado particular. Por ejemplo, puedes configurar un geovallado que permita descubrir un dispositivo que publique un servicio "Aware_File_Share_Service_Name" que no se acerque a menos de 3 metros (especificado como 3,000 mm) y no se aleje a más de 10 metros (especificado como 10,000 mm).

Para habilitar el geovallado, tanto el publicador como el suscriptor deben tomar medidas:

  • El publicador debe habilitar el rango en el servicio publicado con setRangingEnabled(true).

    Si el publicador no habilita el rango, se ignoran las restricciones de geovallado especificadas por el suscriptor y se realiza el descubrimiento normal, sin tener en cuenta la distancia.

  • El suscriptor debe especificar un geovallado con alguna combinación de setMinDistanceMm y setMaxDistanceMm.

    Para cualquier valor, si no se especifica una distancia, esto implica que no hay un límite. Si solo se especifica la distancia máxima, esto implica una distancia mínima de 0. Si solo se especifica la distancia mínima, esto implica que no hay un máximo.

Cuando se descubre un servicio de app similar dentro de un geovallado, se activa la devolución de llamada onServiceDiscoveredWithinRange, que proporciona la distancia medida a la app similar. Luego, se puede llamar a la API de Wi-Fi RTT directa según sea necesario para medir la distancia más adelante.