Sunday, January 20, 2019

Create peer-to-peer connections over Wi-Fi (Android)


This post is an extension of the conceptual explanation found on the same at P2Feed. They also felt that documentation provided by Google was not comprehensive enough. There are number of libraries/starter projects developed by other developers for the same but if you are interested in whats happening under the hood and want to have full control of your peer-to-peer connection with no cost, please do keep on reading. This article assumes that you have already read the Android Developer Documentation on Wifi direct and is here for more clarity.

Wi-Fi Direct allows devices to connect directly to each other using methods similar to traditional Wi-Fi, except without a pre-established access point. Instead, they negotiate to establish one peer device to act as a software access point for all the other devices in the group. Devices can connect simultaneously to a Wi-Fi Direct group and a traditional access point. There are two ways to make a peer-to-peer connection,

1. Service discovery
2. Peer discovery

Peer discovery finds all nearby peers with WiFi Direct. Service discovery finds all nearby peers with WiFi Direct that are running the same service. i.e. With service discovery you can also know whether the peer has a certain application installed and is ready to be paired. One other advantage is one device can have more than one such connection. In this article we will only discuss how you could use service discovery to connect to peers.


1. Setup P2P Manager

private void setupP2PManager() {
mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
mChannel = mManager.initialize(this, getMainLooper(), new WifiP2pManager.ChannelListener() {
@Override
public void onChannelDisconnected() {
Log.i(TAG, "Channel disconnected");
}
});
}

2. Start local service

private void startLocalService() {

/*
* We are generating the buddy name by appending a random number instead of the device name to keep the buddy name unique at most.
* Because buddy name represents a Wifi direct service to which one could connect to. One device can have more than one such service.
* */
String buddyName = "APP_NAME" + String.valueOf((int)(Math.random()*1000));

// You may display the buddy name in your app so you will know what service to select from the second device

// Create a string map containing information about your service.
Map record = new HashMap();
record.put(KEY_BUDDY_NAME, buddyName);

// Service information. Pass it an instance name, service type
// _protocol._transportlayer , and the map containing
// information other devices will want once they connect to this one.
WifiP2pDnsSdServiceInfo serviceInfo =
WifiP2pDnsSdServiceInfo.newInstance("_appname", "_presence._tcp", record);

mManager.addLocalService(mChannel, serviceInfo, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.i(TAG, "Local service added");
}

@Override
public void onFailure(int arg0) {
Log.i(TAG, "Could not add local service");
}
});
}

3. Setup a broadcast receiver

private void setupBroadcastReceiver() {
// Indicates a change in the Wi-Fi P2P status.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);

// Indicates a change in the list of available peers.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);

// Indicates the state of Wi-Fi P2P connectivity has changed.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);

// Indicates this device's details have changed.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
}

private void registerWDBroadcastReceiver() {
receiver = new WDBroadcastReceiver();
receiver.activity = this;
receiver.mChannel = mChannel;
receiver.mManager = mManager;
registerReceiver(receiver, intentFilter);

discoverService();
}

public class WDBroadcastReceiver extends BroadcastReceiver {

private static final String TAG = "WDBroadcastReceiver";

public WDMainActivity activity;
public WifiP2pManager.Channel mChannel;
public WifiP2pManager mManager;

@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
// Determine if Wifi P2P mode is enabled or not, alert
// the Activity.
int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
activity.setIsWifiP2pEnabled(true);
} else {
activity.setIsWifiP2pEnabled(false);
}
} else 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()

} else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

// Connection state changed! We should probably do something about
// that.

if (mManager == null) {
return;
}

NetworkInfo networkInfo = (NetworkInfo) intent
.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

if (networkInfo.isConnected()) {

// We are connected with the other device, request connection
// info to find group owner IP

mManager.requestConnectionInfo(mChannel, activity);
}
} else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
}
}
}

4. Start discovering services

private void discoverService() {
WifiP2pManager.DnsSdTxtRecordListener txtListener = new WifiP2pManager.DnsSdTxtRecordListener() {
@Override
/* Callback includes:
* fullDomain: full domain name: e.g "printer._ipp._tcp.local."
* record: TXT record dta as a map of key/value pairs.
* device: The device running the advertised service.
*/

public void onDnsSdTxtRecordAvailable(String fullDomain, Map record, WifiP2pDevice device) {
Log.d(TAG, "DnsSdTxtRecord available -" + record.toString());
if (record.containsKey(KEY_BUDDY_NAME)) {
buddies.put(device.deviceAddress, record);
}
}
};

WifiP2pManager.DnsSdServiceResponseListener servListener = new WifiP2pManager.DnsSdServiceResponseListener() {
@Override
public void onDnsSdServiceAvailable(String instanceName, String registrationType,
WifiP2pDevice resourceType) {
if (buddies
.containsKey(resourceType.deviceAddress) && !peers.contains(resourceType)) {

Map record = buddies.get(resourceType.deviceAddress);
resourceType.deviceName = record.get(KEY_BUDDY_NAME).toString();
peers.add(resourceType);
// You may show the peers in your UI so you could select to pair

Log.d(TAG, "onBonjourServiceAvailable " + instanceName);
}
}
};

mManager.setDnsSdResponseListeners(mChannel, servListener, txtListener);

WifiP2pDnsSdServiceRequest serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
mManager.addServiceRequest(mChannel,
serviceRequest,
new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.i(TAG, "Service request added successfully");
}

@Override
public void onFailure(int code) {
Log.i(TAG, "Failed to add service request");
}
});

mManager.discoverServices(mChannel, new WifiP2pManager.ActionListener() {

@Override
public void onSuccess() {
Log.i(TAG, "Started discovering services");
}

@Override
public void onFailure(int code) {
Log.i(TAG, "Failed to start discovering services");
handleActionListenerFailure(code);
}
});
}

private void handleActionListenerFailure(int code) {
if (code == WifiP2pManager.P2P_UNSUPPORTED) {
Log.d(TAG, "P2P isn't supported on this device.");
} else if (code == WifiP2pManager.BUSY) {
Log.d(TAG, "The system is to busy to process the request.");
} else if (code == WifiP2pManager.ERROR) {
Log.d(TAG, "The system is to busy to process the request.");
}
}

5. Connect to peer

private void connect(WifiP2pDevice device) {
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = device.deviceAddress;
config.wps.setup = WpsInfo.PBC;

mManager.connect(mChannel, config, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Toast.makeText(WDMainActivity.this, "Peer connection initiated. Please wait...", Toast.LENGTH_SHORT).show();
}

@Override
public void onFailure(int arg0) {
Log.i(TAG, "Failed to initiate peer connection");
handleActionListenerFailure(arg0);
}
});
}

6. Detect changes in connection and exchange IP addresses

For this you need to implement WifiP2pManager.ConnectionInfoListener

@Override
public void onConnectionInfoAvailable(WifiP2pInfo info) {
Log.i(TAG, "onConnectionInfoAvailable");

if (!info.groupFormed) {
return;
}

Toast.makeText(this, "Group formed", Toast.LENGTH_SHORT).show();

if (info.isGroupOwner) {
shouldStartServer();
} else {
shouldConnectToServer(info.groupOwnerAddress.getHostAddress());
}
}

private void shouldStartServer() {
Log.i(TAG, "shouldStartServer");

// You are the group owner. You should start a TCP server so your peers could connect and exchange IP addresses
}

private void shouldConnectToServer(String ip) {
Log.i(TAG, "shouldConnectToServer: " + ip);

// You are not the group owner. You should connect to the TCP server running in group owner to exchange the IP addresses
}

Every phone has a MAC address and an IP address. By the nature of the WiFi Direct connection, the group owner is aware of all the clients MAC addresses, but the clients are not aware of each other. In order to get clients to talk to each other, they must be aware of each other. To solve this problem, we needed to make the group owner distribute a list of the clients MAC and IP addresses to all clients. Unfortunately, the Wi-Fi Direct API does not give the group owner direct access to the IP addresses of the clients. The group owner only has access to its clients’ MAC addresses. To get access the clients’ IP addresses, initiate a socket connection from the client to the group owner.


Additional setup

1. Unregistering the broadcast receiver onPause()

@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume: ");
registerWDBroadcastReceiver();
}

@Override
public void onPause() {
super.onPause();

unregisterReceiver(receiver);
}

2. Cleaning up onDestroy()

private void cleanup() {
mManager.clearLocalServices(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.i(TAG, "Local services cleared");
}

@Override
public void onFailure(int arg0) {
Log.i(TAG, "Could not clear local services");
}
});

mManager.clearServiceRequests(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.i(TAG, "Service requestes cleared");
}

@Override
public void onFailure(int arg0) {
Log.i(TAG, "Could not clear service requests");
}
});
}

3. If you would like to disconnect from existing groups you may do this

mManager.removeGroup(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
Log.i(TAG, "Successfully removed existing group");
initConnection();
}

@Override
public void onFailure(int i) {
Log.i(TAG, "Failed to remove group. It is possible that none exists");
initConnection();
}
});

Do not forget to read P2Feed's notes as they have shared some pain points in the peer-to-peer connection.

Complete code










No comments: