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

Local networking using NSD (Network service discovery)

nsd_blog

In this post we will see how data can be transferred between two android devices in the same network. What follows is a bit of theory and implementation in android studio. There is a source code link at the bottom of the page for reference.

In LAN communication a server listens for client requests in one port (with a blocking connection) and client connects and communicates with server using the IP address and port information. This is basic socket communication in client-server architecture.

LAN communication have been around since ages, be it between laptops, desktops or other mobile devices. Socket communication is nothing new in the world on networking. However, there are mainly 2 problems associated with it:

  • Device(s) is/are not aware of the IP address of other device(s).
  • Device(s) do not know the port other device(s) is/are listening on.

The 2nd problem mentioned above is solvable by prefixing a port. Say port 35627 is fixed for our particular application, so once we know the IP address we know the port we have fixed for our application. But this has other side-effects. OS usually gets any free port when any application requests it or for its own purposes. If the fixed port is used by any other application and you try to use it, you will get a port already in use exception. To avoid this, the port must be requested dynamically from the OS, and OS will provide you any free port (hence no chances of port already in use exception).

And other device(s) must be aware of this port and IP information for communicating with it.

With NSD (Network service discovery), this very problem is addressed. Communication happens over socket like always, but NSD equips us with ways to share the IP and port information. Lets see what NSD is all about, and how to solve the mentioned problem using NSD.

Android NSD allows our app to identify devices in the same network that offer the services we are requesting. With NSD we can register, discover and connect with our service (or other services) over network. Service is basically some information that is advertised over network. Similar to a broadcast receiver and intent broadcast in android. Device 1 advertises some service say Multi-player chess and device 2 discovers it, connect with it and LAN chess use case solved.

Basic steps involved in NSD are depicted in below figure:

nsd1nsd2

As you can see from above, steps involved in NSD are as follows:

  • Application starts and registers a service over network (optional step, avoid if all you want is to discover a service).
  • App starts service discovery over network.
  • In callback method a service object is received when service is FOUND.
  • In callback method a service object is received when service is LOST.
  • resolve the found service and extract the port and IP information of the service advertiser.
  • Use socket to establish a connection and communicate.

Once port and IP information is retrieved by resolving the found service, the socket communication (yes the ye-old socket communication) is used to transfer data. Lets see these are done in code.

Registering your service

Android’s NsdManager class is used for this. simply call registerService.

NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setPort(port);
serviceInfo.setServiceName(mServiceName);
serviceInfo.setServiceType(SERVICE_TYPE);
mNsdManager.registerService(
      serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);

An NsdServiceInfo object needs to be created and passed in register service method for registration. It holds the service information to be advertised. Service name can be any name you want your service to have, a simple string. Service type is also a string and needs to be in “_<protocol>._<transportLayer>” format. You can choose the type of service from IANA. For the demo I created I simply used an unregistered one “_localdash._tcp“.

Next parameter is protocol, please use the one mentioned in the code, I didn’t find any other protocol. DNS_SD stands for Domain Name System Service Discovery.

The registration listener passed as the last parameter in register service method is callback. It contains 4 methods related to registration. Registration listener’s callback methods are when registration is successful, or failed, or un-registration happens etc. Click on this link to know more.

Callback on Registration listener’s onServiceRegistered() method means service has been successfully advertised. Next is discovering the advertised services.

Discovering services

For discovering services using NSD we use the discoverServices() method (of course).

mNsdManager.discoverServices(
        SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);

Here we are telling NsdManager which type of services to discover. The callback here is Discovery listener. This callback informs us when a service is found or lost. It has many callback methods, the one that is important to us is onServiceFound(). Click here to know about other callback methods of discovery listener. For every failure method there is a reason code in parameter. The service found here can be a different service than what we are expecting make sure to verify service type and service name before proceeding. The source for my demo app is shared on github, do check it out to know how to do it (github link). The NsdServiceInfo object in onServiceFound() method cannot be used yet. It needs to be resolved first.

Resolving discovered services

Services can be resolved via resolveService() method of NsdManager. Here is the code for it:

mNsdManager.resolveService(service, mResolveListener)

This is called in service found method of discovery listener (after confirming it is our service). The call back Resolve listener has two methods. on service resolved and on resolve failed (with error code). Once service resolved callback is received, we can use the service info object in its parameter to retrieve IP and port information. Here is how to do it:

NsdServiceInfo serviceInfo = //The service object in OnServiceResolved
String ipAddress = serviceInfo.getHost().getHostAddress();
int port = serviceInfo.getPort();
//Data sending service
DataSender.sendCurrentDeviceData(LocalDashNSD.this, ipAddress, port);

Here we got the service advertisers IP address and port information set by him (or it, the advertiser) when advertising the service. Now Bang!!! socket communication can happen. This happened without any mischievous stuff or hard coding or any magic numbers. Everything is real dynamic.

If you don’t know how to perform basic socket communication or you want to check out all the steps we discussed here check out my demo app code on github:

Happy coding!!!

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

-Kaushal D (@drulabs twitter/github)

drulabs@gmail.com