Wi-Fi aware overview

Wi-Fi Aware capabilities enable devices running Android 8.0 (API level 26) and higher to discover and connect directly to each other without any other type of connectivity between them. Wi-Fi Aware is also known as Neighbor Awareness Networking (NAN).

Wi-Fi Aware networking works by forming clusters with neighboring devices, or by creating a new cluster if the device is the first one in an area. This clustering behavior applies to the entire device and is managed by the Wi-Fi Aware system service; apps have no control over clustering behavior. Apps use the Wi-Fi Aware APIs to talk to the Wi-Fi Aware system service, which manages the Wi-Fi Aware hardware on the device.

The Wi-Fi Aware APIs let apps perform the following operations:

  • Discover other devices: The API has a mechanism for finding other nearby devices. The process starts when one device publishes one or more discoverable services. Then, when a device subscribes to one or more services and enters the publisher's Wi-Fi range, the subscriber receives a notification that a matching publisher has been discovered. After the subscriber discovers a publisher, the subscriber can either send a short message or establish a network connection with the discovered device. Devices can be both publishers and subscribers.

  • Create a network connection: After two devices have discovered each other, either through Wi-Fi Aware discovery or some other mechanism like Bluetooth or BLE, they can create a bi-directional Wi-Fi Aware network connection without an access point.

Wi-Fi Aware network connections support higher throughput rates across longer distances than Bluetooth connections. These types of connections are useful for apps that share large amounts of data between users, such as photo-sharing apps.

Initial setup

To set up your app to use Wi-Fi Aware discovery and networking, perform the following steps:

  1. Request the following permissions in your app's manifest:
<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_COARSE_LOCATION" />
  1. Check whether the device supports Wi-Fi Aware with the PackageManager API, as shown below:

Kotlin

context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)

Java

context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE);
  1. Check whether Wi-Fi Aware is currently available. Wi-Fi Aware may exist on the device, but may not be currently available because the user has disabled Wi-Fi. Depending on their hardware and firmware capabilities, some devices may not support Wi-Fi Aware if Wi-Fi Direct, SoftAP, or tethering is in use. To check whether Wi-Fi Aware is currently available, call isAvailable().

    The availability of Wi-Fi Aware can change at any time. Your app should register a BroadcastReceiver to receive ACTION_WIFI_AWARE_STATE_CHANGED, which is sent whenever availability changes. When your app receives the broadcast intent, the app should check the current state of availability and adjust its behavior accordingly. For example:

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) {
        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) {
        if (wifiAwareManager.isAvailable()) {
            ...
        } else {
            ...
        }
    }
};
context.registerReceiver(myReceiver, filter);

For more information, see Broadcasts.

Obtain a session

To start using Wi-Fi Aware, your app must obtain a WifiAwareSession by calling attach(). This method does the following:

  • Turns on the Wi-Fi Aware hardware.
  • Joins or forms a Wi-Fi Aware cluster.
  • Creates a Wi-Fi Aware session with a unique namespace that acts as a container for all discovery sessions created within it.

If the app attaches successfully, the system executes the onAttached() callback. This callback provides a WifiAwareSession object that your app should use for all further session operations. An app can use the session to publish a service or subscribe to a service.

Your app should call attach() only once. If your app calls attach() multiple times, the app receives a different session for each call, each with its own namespace. This could be useful in complex scenarios, but should generally be avoided.

Publish a service

To make a service discoverable, call the publish() method, which takes the following parameters:

  • PublishConfig specifies the name of the service and other configuration properties, such as match filter.
  • DiscoverySessionCallback specifies the actions to execute when events occur, such as when the subscriber receives a message.

Here's an example:

Kotlin

val config: PublishConfig = PublishConfig.Builder()
        .setServiceName(AWARE_FILE_SHARE_SERVICE_NAME)
        .build()
mAwareSession.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();

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

If publication succeeds, then the onPublishStarted() callback method is called.

After publication, when devices running matching subscriber apps move into the Wi-Fi range of the publishing device, the subscribers discover the service. When a subscriber discovers a publisher, the publisher does not receive a notification; if the subscriber sends a message to the publisher, however, then the publisher receives a notification. When that happens, the onMessageReceived() callback method is called. You can use the PeerHandle argument from this method to send a message back to the subscriber or create a connection to it.

To stop publishing the service, call DiscoverySession.close(). Discovery sessions are associated with their parent WifiAwareSession. If the parent session is closed, its associated discovery sessions are also closed. While discarded objects are closed as well, the system doesn't guarantee when out-of-scope sessions are closed, so we recommend that you explicitly call the close() methods.

Subscribe to a service

To subscribe to a service, call the subscribe() method, which takes the following parameters:

  • SubscribeConfig specifies the name of the service to subscribe to and other configuration properties, such as match filter.
  • DiscoverySessionCallback specifies the actions to execute when events occur, such as when a publisher is discovered.

Here's an example:

Kotlin

val config: SubscribeConfig = SubscribeConfig.Builder()
        .setServiceName(AWARE_FILE_SHARE_SERVICE_NAME)
        .build()
mAwareSession.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();

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

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

If the subscribe operation succeeds, the system calls the onSubscribeStarted() callback in your app. Because you can use the SubscribeDiscoverySession argument in the callback to communicate with a publisher after your app has discovered one, you should save this reference. You can update the subscribe session at any time by calling updateSubscribe() on the discovery session.

At this point, your subscription waits for matching publishers to come into Wi-Fi range. When this happens, the system executes the onServiceDiscovered() callback method. You can use the PeerHandle argument from this callback to send a message or create a connection to that publisher.

To stop subscribing to a service, call DiscoverySession.close(). Discovery sessions are associated with their parent WifiAwareSession. If the parent session is closed, its associated discovery sessions are also closed. While discarded objects are closed as well, the system doesn't guarantee when out-of-scope sessions are closed, so we recommend that you explicitly call the close() methods.

Send a message

To send a message to another device, you need the following objects:

To send a message, call sendMessage(). The following callbacks might then occur:

  • When the message is successfully received by the peer, the system calls the onMessageSendSucceeded() callback in the sending app.
  • When the peer receives a message, the system calls the onMessageReceived() callback in the receiving app.

Though the PeerHandle is required to communicate with peers, you should not rely on it as a permanent identifier of peers. Higher-level identifiers can be used by the application--embedded in the discovery service itself or in subsequent messages. You can embed an identifier in the discovery service with the setMatchFilter() or setServiceSpecificInfo() method of PublishConfig or SubscribeConfig. The setMatchFilter() method affects discovery, whereas the setServiceSpecificInfo() method does not affect discovery.

Embedding an identifier in a message implies modifying the message byte array to include an identifier (for example, as the first couple of bytes).

Create a connection

There are two ways to create a Wi-Fi Aware connection. The first way assumes that you have used Wi-Fi Aware to discover the other device and you have the other device's PeerHandle. The second way assumes that you have discovered the other device's MAC address through some other mechanism, such as Bluetooth or BLE; this is known as out-of-band discovery, or OOB.

Regardless of which method you choose, there are always two devices in a Wi-Fi Aware connection: an initiator and a responder. If you're using Wi-Fi Aware discovery, then the roles are fixed and don't need to be explicitly specified: the subscriber is the initiator and the publisher is the responder. If you are using out-of-band discovery, then the devices need to negotiate these roles on their own.

To create a connection, complete the following sequence of steps:

  1. Create a network specifier:

  2. Build a network request, setting the transport mechanism to TRANSPORT_WIFI_AWARE and the network specifier to the value created in step 1:

Kotlin

var myNetworkRequest: NetworkRequest = NetworkRequest.Builder()
        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
        .setNetworkSpecifier(networkSpecifier)
        .build()

Java

    NetworkRequest myNetworkRequest = new NetworkRequest.Builder()
         .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
         .setNetworkSpecifier(networkSpecifier)
         .build();
  1. Call requestNetwork() and provide the following callback methods:

Kotlin

mCallback = object : ConnectivityManager.NetworkCallback() {

    override fun onAvailable(network: Network) {
        ...
    }

    override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
        ...
    }

    override fun onLost(network: Network) {
        ...
    }
}
mConnMgr.requestNetwork(networkRequest, mCallback)

Java

mCallback = new ConnectivityManager.NetworkCallback() {
    @Override
    public void onAvailable(Network network) {
        ...
    }
    @Override
    public void onLinkPropertiesChanged(Network network,
            LinkProperties linkProperties) {
        ...
    }
    @Override
    public void onLost(Network network) {
        ...
    }
};
    mConnMgr.requestNetwork(networkRequest, mCallback);

The appropriate callback methods are called when the network connection is available, changed, or lost.

  1. When you're finished with the network connection, call unregisterNetworkCallback().

Transfer data

After a connection is established, you can transfer data between the devices with sockets. Wi-Fi Aware connections use IPv6.

The basic steps for transferring data are as follows:

  1. Create a ServerSocket. This socket waits for a connection from a client on a specified port and blocks until the connection occurs, so do this in a background thread.

Kotlin

mCallback = object : ConnectivityManager.NetworkCallback() {
    ...
    override fun onLinkPropertiesChanged(network: Network?, lp: LinkProperties?) {
        val awareNi: NetworkInterface = NetworkInterface.getByName(lp?.interfaceName)
        var ipv6: Inet6Address? = null
        val inetAddresses: Enumeration = awareNi.inetAddresses
        while (inetAddresses.hasMoreElements()) {
            val addr = inetAddresses.nextElement()
            if (addr is Inet6Address && addr.isLinkLocalAddress) {
                ipv6 = addr
                break
            }
        }

        ipv6?.run {
            // should be done in a separate thread
            val ss = ServerSocket(0, 5, ipv6)
            val port: Int = ss.localPort
        }
    }
    ...
}

Java

mCallback = new ConnectivityManager.NetworkCallback() {
    ...
    @Override
    public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
        NetworkInterface awareNi = NetworkInterface.getByName(
                                                     lp.getInterfaceName());
        Inet6Address ipv6 = null;
        Enumeration inetAddresses = awareNi.getInetAddresses();
        while (inetAddresses.hasMoreElements()) {
               InetAddress addr = inetAddresses.nextElement();
               if (addr instanceof Inet6Address) {
                     if (((Inet6Address) addr).isLinkLocalAddress()) {
                             ipv6 = (Inet6Address) addr;
                             break;
                     }
               }
        }
        // should be done in a separate thread
        ServerSocket ss = new ServerSocket(0, 5, ipv6);
        int port = ss.getLocalPort();
    }
    ...
};
  1. Create a client Socket. The client uses the IP address and port of the server socket to connect to the server device.

Kotlin

mCallback = object: ConnectivityManager.NetworkCallback() {
    ...
    override fun onLinkPropertiesChanged(network: Network?, lp: LinkProperties?) {
        // should be done in a separate thread
        // obtain server IPv6 and port number out-of-band
        val cs: Socket? = network?.socketFactory.createSocket(serverIpv6, serverPort)
                ?: null
    }
    ...
}

Java

mCallback = new ConnectivityManager.NetworkCallback() {
    ...
    @Override
    public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
        // should be done in a separate thread
        // obtain server IPv6 and port number out-of-band
        Socket cs = network.getSocketFactory().createSocket(serverIpv6, serverPort);
    }
    ...
};
  1. Send data from the client to the server. When the client socket successfully connects to the server socket, you can send data from the client to the server with byte streams.

  2. The server socket waits for a client connection (with the accept() method). This call blocks until a client connects, so call it in another thread. When a client connects, the server device can receive the data from the client. Carry out any actions with this data, such as saving it to a file or presenting it to the user.

The device opening the client socket (the client device) needs to know the IPv6 address and port number of the server socket. These items must be communicated from the server device to the client device using out-of-band mechanisms, such as Wi-Fi Aware messaging.

Ranging peers and location-aware discovery

A device with Wi-Fi RTT location capabilities can directly measure distance to peers and use this information to constrain Wi-Fi Aware service discovery.

The Wi-Fi RTT API allows direct ranging to a Wi-Fi Aware peer using either its MAC address or its PeerHandle.

Wi-Fi Aware discovery can be constrained to only discover services within a particular geofence. For example, you can set up a geofence that allows discovery of a device publishing an "Aware_File_Share_Service_Name" service that is no closer than 3 meters (specified as 3,000 mm) and no further than 10 meters (specified as 10,000 mm).

To enable geofencing, the publisher and the subscriber both must take action:

  • The publisher must enable ranging on the published service using setRangingEnabled(true).

    If the publisher doesn’t enable ranging, then any geofence constraints specified by the subscriber are ignored and normal discovery is performed, ignoring distance.

  • The subscriber must specify a geofence using some combination of setMinDistanceMm and setMaxDistanceMm.

    For either value, an unspecified distance implies no limit. Only specifying the maximum distance implies a minimum distance of 0. Only specifying the minimum distance implies no maximum.

When a peer service is discovered within a geofence, the onServiceDiscoveredWithinRange callback is triggered, which provides the measured distance to the peer. The direct Wi-Fi RTT API can then be called as necessary to measure distance at later times.