WiFi direct service discovery in android

wi-fi-direct-service-discovery

I suggest reading my previous posts on NSD and WiFi Direct before reading this one. This post requires some of things discussed in these two posts. If you are already aware of WiFi direct service discovery, you  can directly check out my sample code on git.

Like NSD, we can register and discover services over WiFi direct. The problem I faced with WiFi direct was one prefixed port for initial data transfer (as peer devices were not aware of the port information), after that the port was dynamic, and fixed port was released. Using WiFi direct service discovery we can append additional data (100-200 bytes)  with the advertised service. So unlike WiFi direct, we can request port dynamically and append it with service. No prefixed port.

We all know socket communication, here is a recap just in case:

//Server side
ServerSocket mServer = new ServerSocket(mPort);
Socket socket = null;
while (acceptRequests) {
    // this is a blocking operation
    socket = mServer.accept();
    handleData(socket);
}

//Client side
socket = new Socket(hostIP, hostPort);
OutputStream os = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(transferObject);
oos.close();

If you have read my earlier post on WiFi direct, you know almost everything required for WiFi direct service discovery (except appending additional data part). Function is similar to NSD and code is similar to WiFi-Direct.

Adding local service

Same class as WiFi direct is used. Here we use WifiP2pmanager‘s addLocalService() method. Here is the official documentation for this method:

wifip2pmanager_addlocalservice

All the parameters are familiar except for WifiP2pServiceInfo. Similar to NSD, this is a holder object for service information. In this case it is a WiFi P2P service info.

WifiP2pServiceInfo is a class for storing service information that is advertised over a WiFi peer-to-peer setup. It has two direct sub-classes. Both are bonjour service info:

For our use case, which sharing of port and IP information and then finally sharing data with other devices, we will use the former, it allows us to append a string map with the service. The later allows us to append a list of string, which can also be used.

As mentioned earlier, WifiP2pDnsSdServiceInfo allows us to append a string map. here is how to do it.

Map<String, String> record = new HashMap<String, String>();
record.put(KEY_BUDDY_NAME, player == null ? Build.MANUFACTURER : player);
record.put(KEY_PORT_NUMBER, String.valueOf(port));
record.put(KEY_DEVICE_STATUS, "available");
record.put(KEY_WIFI_IP, Utility.getWiFiIPAddress(context));

WifiP2pDnsSdServiceInfo service = WifiP2pDnsSdServiceInfo.newInstance(
    SERVICE_INSTANCE, SERVICE_TYPE, record);
wifiP2pManager.addLocalService(wifip2pChannel, service, new WifiP2pManager.ActionListener() {

    @Override
    public void onSuccess() {
        Log.d(TAG, "Added Local Service");
    }

    @Override
    public void onFailure(int error) {
        Log.e(TAG, "ERRORCEPTION: Failed to add a service");
    }
});

As you can see in the sample code above, a map called record is passed when creating instance of service info object. After this you are done with advertising your WiFi direct service.

Discovering WiFi direct services

Discovery requires adding a service discovery request in WiFi direct via WifiP2pManager‘s addServiceRequest() method. Here is description from the official site:

wifip2pmanager_addservicerequest

After this a the service discovery request must be issued. Set the type of service you want to discover DNS-SD or UPNP and issue the request.

serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
wifiP2pManager.addServiceRequest(wifip2pChannel, serviceRequest,
new WifiP2pManager.ActionListener() {

    @Override
    public void onSuccess() {
        Log.d(TAG, "Added service discovery request");
    }

    @Override
    public void onFailure(int arg0) {
        Log.d(TAG, "ERRORCEPTION: Failed adding service discovery request");
    }
});
wifiP2pManager.discoverServices(wifip2pChannel, new WifiP2pManager.ActionListener() {

	@Override
	public void onSuccess() {
		Log.d(TAG, "Service discovery initiated");
	}

	@Override
	public void onFailure(int arg0) {
		Log.d(TAG, "Service discovery failed: " + arg0);
	}
});

There is a reason code associated with every failure callback. Check those error code if you get a failure callback.

The map that was advertised is received in DnsSdTxtRecordListener interface callback. This needs to be set to receive the map. And DnsSdServiceResponseListener is callback for receiving the advertised service. Here is how to set it:

wifiP2pManager.setDnsSdResponseListeners(wifip2pChannel,
new WifiP2pManager.DnsSdServiceResponseListener() {

    @Override
    public void onDnsSdServiceAvailable(String instanceName,
        String registrationType, WifiP2pDevice srcDevice) {

        // A service has been discovered. Is this our app?
        if (instanceName.equalsIgnoreCase(SERVICE_INSTANCE)) {
            // yes it is
         } else {
            //no it isn't
        }
    }
}, new WifiP2pManager.DnsSdTxtRecordListener() {

    @Override
    public void onDnsSdTxtRecordAvailable(
            String fullDomainName, Map<String, String> record,
            WifiP2pDevice device) {
        boolean isGroupOwner = device.isGroupOwner();
        peerPort = Integer.parseInt(record.get(TransferConstants.KEY_PORT_NUMBER).toString());
			// further process
    }
});

here in the text record callback you can see the map is received with whatever info was set. After this it is same as WiFi direct. you can check out my earlier post for this.

You need to have a connection info listener same as WiFi direct example (old post), and you need to connect with the device, with info received as a part of DnsSdTxtRecordAvailable or DnsSdServiceAvailable callbacks (above code example).

In the code example above you can see that the port information is received as a part of string map in text record available method, and in connection info callback you can get the IP address of the group owner of the current WiFi group. After that, its the same age old socket communication.

Check out the google sample for this. My sample app source is available on git.

Happy coding!!!

Let me know what you think. Got any questions? shoot below in comments.

-Kaushal D (@drulabs twitter/github)

drulabs@gmail.com

Local networking in android – WiFi direct

wifi-direct

In my earlier blog post I discussed data sharing between two android devices in same network using NSD. In this post I will explain communication between two non-connected android devices (can be connected to same or other network, doesn’t really matter) via WiFi direct. Devices should be in WiFi range. I will start with a bit of theory about WiFi direct and then we will see how it is implementable using android APIs (Sample app source code git link is at the end of this post).

Same as earlier post the problem addressed is sharing IP and port information. Communication again will be socket communication.

WiFi direct is a WiFi certified standard enabling devices to connect with each other without requiring a wireless access point (a.k.a router or WiFi hot spot). Using this, devices can communicate with each other at typical WiFi speeds, unlike ad hoc wireless connections (which allows two or more devices to connect and communicate, and has a limit of 11 Mbps) or Bluetooth, setup required for WiFi direct is much simpler. Here each member is assigned a limited access point and other members connect to it as regular clients. WPS and WPA2 are used for encryption and keep the communication private.

The peer device acting as current access point is said to be assuming a group owner role in WiFi direct group. A WiFi direct group consists of 1 group owner and devices or peers connected to it as clients (P2P clients). The group owner device sets properties for communication like operating channel, whether the group is persistent, encryption type etc. Any compatible device with right hardware and android ICS or above (API 14+) can assume group owner role. After role negotiation (group owner or P2P client), devices assume their decided role, and group owner starts operating in access point mode (this access point will not be visible under available networks of mobile devices).

Now coming to android. Android’s WiFi P2P framework complies with WiFi direct certification program. It consists of:

  • Methods that allows us to discover, request and connect to peers (android.net.wifi.p2p.WifiP2pManager).
  • Listeners that notifies us of success and failure of WifiP2pmanager’s method calls.
  • Intents to notify specific events, such as new peer joined, connection dropped etc. (like WIFI_P2P_PEERS_CHANGED_ACTION and WIFI_P2P_CONNECTION_CHANGED_ACTION)

Discovering peers

You need the following permissions for using WiFi direct for communication

    <uses-permission android:name="android.permission.INTERNET" android:required="true"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" android:required="true"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" android:required="true"/>

Internet permission is required for using sockets anyway.

wifiP2pManager.discoverPeers(wifip2pChannel, new WifiP2pManager.ActionListener() {
    @Override
    public void onSuccess() {
        // discovery has been successfully started
    }

    @Override
    public void onFailure(int reasonCode) {
        // discovery failed to start. checkout reason code
    }
});

The above method is all you need to start the WiFi direct peer discovery. If you get a callback in failure method, check the reason code for it.

Getting peer list

Getting peer list is tricky, as WifiP2pManager will not give you that. This is done via broadcast receivers registered dynamically (not in the manifest file).

wifi-direct-broadcast-receiver-01

The second filter action in above code (the image) is what is triggered when a new peer has joined (or left). When broadcast with this action is received we can request peers from WifiP2pManager. It takes a callback to return entire peer list not just the new ones, so if you are displaying a list, clear the list and add all the peers received in the callback. here is the broadcast receiver’s code:

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() of passed activity
    // the activity implements the listener interface
    if (wiFiP2pManager != null) {
        wiFiP2pManager.requestPeers(channel, activity);
    }
}

The callback that is overridden will be called asynchronously. Peer list can be extracted from there.

@Override
public void onPeersAvailable(WifiP2pDeviceList peerList) {
    List<WifiP2pDevice> devices = (new ArrayList<>());
    devices.addAll(peerList.getDeviceList());

    //do something with the device list
}

So now we have the peer list. We can display this list to user or call connect with each peer. Once the peer is discovered we need to send a connection request to connect and form a WiFi direct group.

Connecting with a peer

This step requires WiFi mac of the device. It is received as a part of peer info in discovery step. Here are the properties of WifiP2pDevice:

wifip2pdevice_states

The WifiP2pDevice list that we received in OnPeersAvailable() method of PeerListListener callback has exactly what we need to make a connection request.

Here is how to make a connection request using WifiP2pManager.

WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = //set device address from WifiP2pDevice received;
config.wps.setup = WpsInfo.PBC;
config.groupOwnerIntent = 4;
wifiP2pManager.connect(wifip2pChannel, config, new WifiP2pManager.ActionListener() {
    @Override
    public void onSuccess() {
        // Connection request successfully sent
    }

    @Override
    public void onFailure(int reasonCode) {
        // Failed to send connection request.
    }
});

Just create a config object set channel properties and call the connect method. The group owner intent value is for setting the probability of the device to become group owner. It’s value varies from 0-15. 15 means highest chance of the connection request sender device to become group owner. But whatever code you write, you must handle both group owner and a regular P2P client scenario.

Once the request is sent to a device, the device user must accept the connection request from system prompt. After that role negotiation happens and devices move to their decided roles. After successful connection, a connection info request can be issued via WifiP2pManager. Which sends callback to WifiP2pManager.ConnectionInfoListener. It has only one method:

wifip2pmanager-connectioninfolistener

This connection info object WifiP2pInfo contains the IP information of group owner. Check below:

wifip2pinfo

So now, our first problem is addressed, here we got the IP address of the group owner. And we also know whether the current device is group owner or not. So basically every peer in the group knows the group owner’s IP address.

The remaining problem is sharing the port it is listening on. I couldn’t find any proper solution for this, I solved it by prefixing the port, then sharing the dynamic port data with clients, and then clients can share their own info and dynamic port. So first communication happens over the fixed port and after that, then dynamic port data number is transferred, and then it is regular socket communication. Check out my sample app for this.

Even this problem can be solved using the WiFi direct service discovery. Watch out for my next post on this topic.

One last thing to cover in this post is method called createGroup(). This method is for supporting the legacy devices with no WiFi direct hardware. This basically an access point and any device with WiFi capability can connect with it like a regular access point and share data like we did in my previous post. On top of that it can work as expected in WiFi direct scenario, so support both types of devices.

Sample app code available in github.

Happy coding!!!

Let me know what you think. Got any questions? shoot below in comments.

-Kaushal D (@drulabs twitter/github)

drulabs@gmail.com