Skip to content

Setting up and implementing WiFi Direct for inter device communication

Evan Vander Hoeven edited this page Oct 7, 2019 · 7 revisions

Table of Contents

Configuring the device to be WiFi Direct Enabled

This will vary by device and by Android version, but to start, enter the settings application. First, click on "connections" and then on WiFi. This will bring you to the screen where you can select WiFi networks. Somewhere in that screen should be a button that reads "Wi-Fi Direct" tapping on that will allow you to see if your WiFi direct is enabled, and what devices you can connect to. This is necessary for the application to work. If your device does not have WiFi Direct settings, then you might not be able to run this application.

Limitations of WiFi Direct

Because Apple doesn't play nice, WiFi Direct only works between android devices, and apple devices, but not between each other. While windows 10 machines can use WiFi Direct to communicate in a fashion similar to this app, we are not sure if we can use WiFi direct in this fashion between a windows 10 machine and an android device.

Data formats for communication

Communication between devices is done in a data stream. As such, we can send data in any format that we want, provided that the format is agreed upon between the two devices beforehand (handled by the app). So, we could create our own classes, or send well known data types like JPG or PNG.

Making the application use WiFi direct

These next steps come from this guide from the Android developers site, so if you would prefer to follow that guide, you can do so. This guide is going to simplify things a bit and be a fair amount more explicit in how we will implement WiFi direct in this application only in Java. Also, there were some things that the android guide is out of date about, that this guide (prototype developed in September of 2019) will be more up-to-date.

Configuring your project

The first step is declaring necessary permissions in the android manifest. In addition to these permissions, the android device will also require location settings to be on, and allowed for this application. (something that has to be done separately at run-time)

<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" />

Add some helper classes to make your life easier

WiFi-Direct Activity abstract class

There are some requirements for our activity if it wants to use WiFi direct that aren't available in the standard Activity class. Therefore we need to create our own. To make this functionality available across multiple activities, our activities will inherit from this class. However, some of this functionality will be tied to the GUI, so it will be abstract, so GUI methods are handled case-by-case.

  • First, make it implement the WiFiP2pManager.ChannelListener interface, which will make the activity respond when the channel disconnects.
  • Second, add some essential class member variables:
    • The WiFiDirectBroadcastReciever is a custom class we create below. They are dependent on each other.
    // Protected variables
    protected IntentFilter intentFilter = new IntentFilter();
    protected WifiP2pManager.Channel channel;
    protected WifiP2pManager manager;
    protected WiFiDirectBroadcastReceiver receiver;

    // Private Variables
    private static final int PERMISSIONS_REQUEST_CODE_ACCESS_COARSE_LOCATION = 1001; // Needed for onRequestPermissionsResult
    private boolean p2pEnabled = false;

    // Getters and Setters
    public void setP2pEnabled(boolean enabled) { this.p2pEnabled = enabled; }
    public boolean isP2pEnabled() { return p2pEnabled; }
  • Third, setup some basic Lifecycle functions which will handle your recievers, as long as child classes call super() anytime they override these methods, they will be save.
    /** register the BroadcastReceiver with the intent values to be matched */
    @Override
    public void onResume() {
        super.onResume();
        receiver = new WiFiDirectBroadcastReceiver(manager, channel, this);
        registerReceiver(receiver, intentFilter);
    }

    /** unregister the BroadcastReceiver so that wi-fi events are not taking up computer time */
    @Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(receiver);
    }
  • Fourth, make an onCreate method to do some setup. This should be overridden in the base class, but calls to super() are required. So we can abstract some of the work for intent filtering for the child classes.
    • onRequestResult() is called by onCreate, so we should put that in there too.
    /**
     * Lifecycle function which instantiates the GUI for
     * first time setup
     * @param savedInstanceState
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Normally, I'd declare the content view here,
        // but that shall be left to the children classes.

        this.manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
        this.channel = manager.initialize(this, getMainLooper(), null);
        this.receiver = new WiFiDirectBroadcastReceiver(this.manager, this.channel, this);

        // Get intents for changes related to using WiFiDirect
        this.intentFilter = new IntentFilter();
        this.intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
        this.intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
        this.intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
        this.intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);

        // Check if the app has permissions to use location data, and ask for it if we don't.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                && checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                    WiFiDirectActivity.PERMISSIONS_REQUEST_CODE_ACCESS_COARSE_LOCATION);
            // We don't handle the response here, we
        }

        // Because there is the potential for the response to have errors handled in the GUI,
        // discover peers ought to be created within the child class. (it's declared abstract)
        this.discoverPeers();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
    {
        // Use of switch case to allow us to expand this method later if needed.
        switch (requestCode) {
            case PERMISSIONS_REQUEST_CODE_ACCESS_COARSE_LOCATION:
                if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                    Log.e("WiFiDirectActivity",
                            "Coarse Location permission not granted. Unable to use WiFi Direct features");
                    finish(); // closes the activity
                }
                break;
            default:
                Log.e("WiFiDirectActivity", "Unhandled permissions result: " + requestCode);
        }
    }
  • Fifth, add some abstract methods that relate to the GUI, since every GUI will be different, these need to be implemented in the child activities of this class.
    /**
     * Required by the ChannelListener interface. Responds to the channel
     * disconnecting in the application.
     */
    @Override
    public void onChannelDisconnected() {
        // default implementation is merely letting the user know.
        // overriding would be recommended to try and connect again.
        Toast.makeText(this, "Channel lost", Toast.LENGTH_SHORT).show();
        Log.e("WiFiDirectActivity", "Channel lost, implementing try again protocol suggested.");
    }

    /**
     * starts a call to WiFiP2pManager.discoverPeers() and describes the behavior of the callback.
     * Each activity must handle their own errors, thus this method is abstract.
     */
    protected abstract void discoverPeers();

    /**
     * takes a list of WiFi P2P devices and displays them on the GUI
     */
    protected abstract void displayPeers(WifiP2pDeviceList wifiP2pDeviceList);

    /**
     * makes a connection to the selected device
     * @param device the information about the device we want to connect to.
     */
    protected abstract void connectToDevice(WifiP2pDevice device);

This isn't the complete class, more needs to be added, some relating to the GUI, some relating to the connection. More work will be added as we go along, but that needs to be customized.

WiFi-Direct BroadcastReciever

Interacting with the network requires asynchronous processing. This asynchronous processing will send events that we need to listen for so that our application can respond to changes in the WiFi state. To do this, we create a subclass of the BroadcastReciever class.

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.p2p.WifiP2pDeviceList;
import android.net.wifi.p2p.WifiP2pManager;
import android.util.Log;

/**
 * A BroadcastReceiver that notifies a WiFiDirectActivity of important Wi-Fi p2p events.
 */
public class WiFiDirectBroadcastReceiver extends BroadcastReceiver {

    private WifiP2pManager manager;
    private WifiP2pManager.Channel channel;
    private WiFiDirectActivity activity;

    public WiFiDirectBroadcastReceiver(WifiP2pManager manager, WifiP2pManager.Channel channel,
                                       WiFiDirectActivity activity) {
        super();
        this.manager = manager;
        this.channel = channel;
        this.activity = activity;
    }

    /**
     * Description pending
     * @param context a Context object that does something I don't know yet.
     * @param intent an Intent object that describes what kinds of events we are listening for.
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        // Variable declaration
        String action = intent.getAction();
        WifiP2pManager.PeerListListener myPeerListListener = new WifiP2pManager.PeerListListener() {
            @Override
            public void onPeersAvailable(WifiP2pDeviceList wifiP2pDeviceList) {
                // take that device list and give it to the activity to display
                // so that the user can choose what device they want to connect to
                Log.i("WFDBroadcastReceiver","Peers available");
                activity.displayPeers(wifiP2pDeviceList);
            }
        };

        if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
            // Check to see if Wi-Fi is enabled and notify appropriate activity
            int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
                // Log the event
                Log.i("WFDBroadcastReceiver","WIFI P2P State Changed to enabled");
                activity.setP2pEnabled(true);
            } else {
                // Log the event
                Log.i("WFDBroadcastReceiver","WIFI P2P State Changed to disabled");
                activity.setP2pEnabled(false);
            }
        } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
            // Log the event
            Log.i("WFDBroadcastReceiver","WIFI P2P Peers Changed");

            // Call WifiP2pManager.requestPeers() to get a list of current peers
            // 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) {
                Log.i("WFDBroadcastReceiver","Requesting Peers");
                manager.requestPeers(channel, myPeerListListener);
            }
            else {
                Log.e("WFDBroadcastReceiver","manager null, cannot retrieve list of peers");
            }
        } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
            // Log the event
            Log.i("WFDBroadcastReceiver","WIFI P2P Connection Changed");
            // Respond to new connection or disconnections
            // Applications can use requestConnectionInfo(), requestNetworkInfo(), or requestGroupInfo() to retrieve the current connection information.
        } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
            // Log the event
            Log.i("WFDBroadcastReceiver","WIFI P2P This Device Changed");
            // Respond to this device's wifi state changing
            // Applications can use requestDeviceInfo() to retrieve the current connection information.
        }
        else {
            Log.e("WFDBroadcastReceiver","Action not recognized. Action: " + action);
        }
    }
}

Implementing the WiFiDirectActivity Child class

Create your activity, and have it extend the abstract class that you made earlier.

public class MainActivity extends WiFiDirectActivity

Starting with onCreate()

So, here we can see the advantages of building the abstract class. The onCreate in the abstract class handled the broadcast receiver, intent filtering, and other one-time details for us. So, the only thing that we need to do in the child class is call the super method and handle the GUI. There is one exception to this rule. You need to have a call to discoverPeers somewhere in this method. In this case we put it inside a button. If discoverPeers is not called, we can't look for people to connect to.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Get references to the GUI objects (dependent on your GUI, you could also use data binding)
        peerListLayout = findViewById(R.id.peerListLayout);
        refreshButton = findViewById(R.id.refresh_button);

        // Create a listener which will ask for a new list of peers
        refreshButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                discoverPeers();
            }
        });
    } // end of onCreate

Implementing required methods

The abstract class required three methods from all children, discoverPeers(), displayPeers(), and connectToDevice(). The reason we need these in the child classes is that they should interact with the GUI to provide the user with information. The more astute of you will notice that these three methods chain together in an order of events. So it really makes sense to implement these methods in order so that you have a better grasp of the order of events at runtime.

Let's take a look at discoverPeers(). There is really only two steps to discoverPeers(), using the WiFiP2pManager to handle peer discovery, and implementing the callback. There is only one way to use the WiFiP2pManager, and that is with WiFiP2pManager.discoverPeers(). So, the more important detail is the callback. The callback has two methods that are required, onSuccess() and onFailure(int reasonCode). You'll see that callback behavior often with the WiFiP2pManager. onSuccess only lets you know that peer discovery completed without error. It doesn't let us know what peers were discovered. That is handled in the broadcast receiver we implemented earlier. In the example below, we just log the success. Though it would also be appropriate to make a Toast for the user. onFailure() is a little more complicated because it has input, the reason for failure. The switch case handles all cases documented in the WiFiP2pManager class, so it would be a good idea to copy the structure directly. Though you could also use a toast as opposed to logging here too.

    public void discoverPeers() {
        // Discover a list of peers we can interact with
        manager.discoverPeers(channel, new WifiP2pManager.ActionListener() {
            @Override
            public void onSuccess() {
                // Only notifies that the discovery process completed.
                // does not provide information about the discovered peers.
                // a WIFI_P2P_PEERS_CHANGED_ACTION intent is broad-casted if
                // we were successful, see WiFiDirectBroadcastManager for state change code.
                Log.i("MainActivity", "Peer Discovery reports success");
            }

            @Override
            public void onFailure(int reasonCode) {
                // Turn the error code into a human readable message.
                // Other error handling may occur in this code.
                String errorType;
                switch (reasonCode) {
                    case WifiP2pManager.ERROR:
                        errorType = "Operation Failed due to an internal error.";
                        break;
                    case WifiP2pManager.P2P_UNSUPPORTED:
                        errorType = "Operation failed because Peer to Peer connections are not supported on this device.";
                        break;
                    case WifiP2pManager.BUSY:
                        errorType = "Operation failed because the framework is busy and is unable to service the request.";
                        break;
                    case WifiP2pManager.NO_SERVICE_REQUESTS:
                        errorType = "Operation failed because no service channel requests were added.";
                        break;
                    default:
                        errorType = "Operation failed due to an unknown error. Reason Code: " + reasonCode;
                        break;
                }
                // log the error with a description of what went wrong.
                Log.e("MainActivity","Peer Discovery Failure. Error: " + errorType);
            }
        });
    }

displayPeers is called in the WiFiDirectBroadcastReceiver that we implemented earlier. The existence of this particular function is why we needed to create our own subclass of activity. So, all it does is takes the list of all peer devices discovered and displays all of their information. Now, if I were to implement this again, I would not use .toString() because there is a lot of information. I'd make a summary of the device information, and then show all information after the user asks for it. This is just example code.

    @Override
    public void displayPeers(WifiP2pDeviceList wifiP2pDeviceList) {
        // Take the list of P2P devices and display relevant information on the screen

        // Parse the device list and put their details as textViews
        peers = wifiP2pDeviceList.getDeviceList().toArray(new WifiP2pDevice[0]);

        // Log some information for sanity check
        Log.i("MainActivity","Size of peer list: " + peers.length);
        // Clear the old peer list
        peerListLayout.removeAllViews();
        // create a new peer list
        int idTracker = 0;

        for (WifiP2pDevice device : peers) {
            // create a TextView for the WiFiP2p device
            TextView label = new TextView(this);
            label.setText(device.toString());
            label.setId(idTracker++);
            label.setClickable(true);
            label.setLayoutParams(new LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
            ));
            // add an onClickListener
            label.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    connectToDevice(peers[view.getId()]);
                }
            });
            // add the TextView to the view
            peerListLayout.addView(label);
        }
    }

connectToDevice() takes one of the devices selected from the peer list displayed in the previous method. Using WiFiP2pManager has the exact same requirements every time. How you handle the callback methods is up to you.

    protected void connectToDevice(WifiP2pDevice device)
    {
        WifiP2pConfig config = new WifiP2pConfig();
        config.deviceAddress = device.deviceAddress;
        manager.connect(channel, config, new WifiP2pManager.ActionListener() {

            @Override
            public void onSuccess() {
                Log.i("MainActivity", "System reports successful connection");
            }

            @Override
            public void onFailure(int reason) {
                Log.e("MainActivity","Connection failure. Reason: " + reason);
            }
        });
    }

Where to go from here?

So, at this point, you have an application that can find peers, select a peer, and connect to said peer. What is missing however, is what happens after that. In the original guide they made an application that could only transfer .jpg files. This is very limited, so how you handle data packets and files is going to be entirely dependent on the application. However you send that data, you are going to do so in this process:

  1. Create a ServerSocket. This socket waits for a connection from a client on a specified port and blocks until it happens, so do this in a background thread.
  2. Create a client Socket. The client uses the IP address and port of the server socket to connect to the server device.
  3. 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. 4.The server socket waits for a client connection (with the accept() method). This call blocks until a client connects, so call this is another thread. When a connection happens, 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.