package eu.hgross.blaubot.android.wifip2p;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.NetworkInfo;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pDevice;
import android.net.wifi.p2p.WifiP2pDeviceList;
import android.net.wifi.p2p.WifiP2pGroup;
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.net.wifi.p2p.WifiP2pManager.ActionListener;
import android.net.wifi.p2p.WifiP2pManager.Channel;
import android.net.wifi.p2p.WifiP2pManager.DnsSdServiceResponseListener;
import android.net.wifi.p2p.WifiP2pManager.DnsSdTxtRecordListener;
import android.net.wifi.p2p.nsd.WifiP2pDnsSdServiceInfo;
import android.net.wifi.p2p.nsd.WifiP2pDnsSdServiceRequest;
import android.net.wifi.p2p.nsd.WifiP2pServiceInfo;
import android.net.wifi.p2p.nsd.WifiP2pUpnpServiceInfo;
import android.net.wifi.p2p.nsd.WifiP2pUpnpServiceRequest;
import android.os.Vibrator;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import eu.hgross.blaubot.android.IBlaubotAndroidComponent;
import eu.hgross.blaubot.android.IBlaubotBroadcastReceiver;
import eu.hgross.blaubot.android.wifip2p.BlaubotWifiP2PBroadcastReceiver.IBlaubotWifiDirectEventListener;
import eu.hgross.blaubot.core.Blaubot;
import eu.hgross.blaubot.core.BlaubotDevice;
import eu.hgross.blaubot.core.IBlaubotAdapter;
import eu.hgross.blaubot.core.IBlaubotDevice;
import eu.hgross.blaubot.core.IUnidentifiedBlaubotDevice;
import eu.hgross.blaubot.core.acceptor.ConnectionMetaDataDTO;
import eu.hgross.blaubot.core.acceptor.IBlaubotIncomingConnectionListener;
import eu.hgross.blaubot.core.acceptor.IBlaubotListeningStateListener;
import eu.hgross.blaubot.core.acceptor.discovery.BeaconMessage;
import eu.hgross.blaubot.core.acceptor.discovery.ExchangeStatesTask;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeacon;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotBeaconStore;
import eu.hgross.blaubot.core.acceptor.discovery.IBlaubotDiscoveryEventListener;
import eu.hgross.blaubot.core.statemachine.BlaubotAdapterHelper;
import eu.hgross.blaubot.core.statemachine.states.IBlaubotState;
import eu.hgross.blaubot.ethernet.BlaubotEthernetConnection;
import eu.hgross.blaubot.ethernet.BlaubotEthernetUtils;
import eu.hgross.blaubot.ethernet.EthernetBeaconAcceptThread;
import eu.hgross.blaubot.ethernet.IEthernetBeacon;
import eu.hgross.blaubot.util.KingdomCensusLifecycleListener;
import eu.hgross.blaubot.util.Log;
/**
* We are able to signal some defined strings (see http://upnp.org/specs/dm/UPnP-dm-BasicManagement-v1-Service.pdf)
* over the upnp framework, so we pick one of them to indicate that there is a blaubot instance running.
*
* The beacons then search for this upnp string/service and connect to them to probe if this devices
* are really blaubot instances. If we find a blaubot instance, they exchange the beaconUUID, and if their beaconIds match
* the usual {@link BeaconMessage} to exchange their states.
*
* Sadly we can not communicate additional attributes via upnp service requests (which we should be according to the upnp standard)
* on android so we have to actively ask for this attributes (beaconId, state) by connecting to the device.
* see: https://code.google.com/p/android/issues/detail?id=40003
*
*
* @author Henning Gross {@literal (mail.to@henning-gross.de)}
*/
public class BlaubotWifiP2PBeacon implements IBlaubotBeacon, IBlaubotBroadcastReceiver, IEthernetBeacon, Closeable, IBlaubotAndroidComponent {
private static final String LOG_TAG = "BlaubotWifiP2PBeacon";
/**
* Debug switch.
* If set to true, the beacon connects to EVERY found wifi direct peer.
* Note that we can get stuck in a connected group, since we assume that the not group-owning
* side closes the group as soon as it has completed the state exchange.
* It also occasionally reboots my TV ;-)
*/
private static final boolean CONNECT_TO_ALL_AVAILABLE_PEERS = false;
/**
* This is the Blaubot service type string used for Bonjour service discovery and advertisement.
*/
private static final String BONJOUR_SERVICE_TYPE = "_blaubot._tcp";
/**
* The string representation of the upnp device urn as per UPnP Device Architecture1.1 format
* http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
*/
private static final String UPNP_DEVICE_URN = "urn:schemas-upnp-org:device:MediaServer:1";
/**
* The string representation of the upnp service urn for this beacon as per UPnP Device Architecture1.1 format
* http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
*/
private static final String UPNP_SERVICE_URN = "urn:schemas-upnp-org:service:ContentDirectory:1";
/**
* Bonjour TXT record for the unique device id
*/
private static final String TXT_RECORD_UNIQUE_DEVICE_ID_KEY = "ID";
/**
* BroadcastReceiver that recieves all relevant WiFiP2P-Events and dispatches it to
* appended listeners
*/
private final BlaubotWifiP2PBroadcastReceiver wifiP2pBroadcastReceiver;
/**
* The wifi p2p channel used for beacon interactions
*/
private final Channel wifiP2pBeaconChannel;
/**
* The WiFiP2PManager
*/
private final WifiP2pManager wifiP2pManager;
/**
* The android vibrator service, if available. May be null.
*/
private Vibrator vibratorService;
/**
* The port to be used for beacon connections, once a p2p group is formed.
* The EthernetBeaconAcceptThread uses this port in conjunction with the
* IEthernetBeacon interface.
*/
private final int beaconPort;
/**
* A listener that keeps track of the devices currently forming a blaubot network via a
* ILifeCycleEventListener to be used in the beaconScanner to reduce the device list that
* needs to be scanned.
*/
private KingdomCensusLifecycleListener kingdomCensusLifeCycleListener;
/**
* The accept thread accepting connections on 0.0.0.0 and the specified beaconPort.
* Is recreated on each stop/start cycle.
* Has access to this very member variable to kill himself, if itself does not match
* this instance.
*/
private volatile EthernetBeaconAcceptThread ethernetBeaconAcceptThread;
private IBlaubotListeningStateListener listeningStateListener;
private IBlaubotIncomingConnectionListener incomingConnectionListener;
private IBlaubotDiscoveryEventListener discoveryEventListener;
private volatile boolean isStarted;
private volatile boolean discoveryActivated;
/**
* A list of devices discovered in the past via bonjour events.
* It is filled by the bonjour listener and later used by the beaconScanner to build
* a list of devices to connect to in a continuous loop to check for state changes.
*/
private Set<BlaubotWifiP2PDevice> knownActiveDevices;
/**
* A scanner that sequentially loops over the knownActive devices filtered by connected
* devices to retrieve states by creating a WifiDirect group to the devices and exchanging
* the states over sockets.
* The scanner is stopped/started with each startListening or stopListening call.
*/
private WifiP2PBeaconScanner beaconScanner;
/**
* Private listener class that is attached to the wifi p2p manager to receive UPnP discovery
* events over WifiDirect without any group connection
*/
private final UPNPListener upnpListener;
/**
* Private listener class that is attached to the wifi p2p manager to receive bonjour discovery
* events over WiFiDirect without any group connection.
*/
private BonjourListener bonjourListener;
/**
* The blaubot instance global beacon store to get and store connection metadata discovered
* from all the beacons.
*/
private IBlaubotBeaconStore beaconStore;
/**
* The Blaubot top level instance to traverse the blaubot components.
*/
private Blaubot blaubot;
/**
* This beacon's uuid to discriminate between multiple Blaubot instances.
*/
private UUID beaconUuid;
/**
* ExecutorService to process some async started tasks in a single threaded manner.
*/
private volatile ExecutorService executorService;
/**
* This Blaubot instance's own device (and uniqueDeviceId).
*/
private IBlaubotDevice ownDevice;
/**
* The last known state of this Blaubot instance.
*/
private IBlaubotState currentState;
/**
* The string that we exploit to pre-filter upnp devices before we probe them if they are really a
* blaubot beacon.
* @param wifiP2pManager android's wifi p2p manager service
* @param beaconChannel the wifi p2p channel to be used for this beacon
* @param beaconPort the tcp port for this beacon to listen on
*/
public BlaubotWifiP2PBeacon(WifiP2pManager wifiP2pManager, Channel beaconChannel, int beaconPort) {
this.beaconPort = beaconPort;
this.knownActiveDevices = Collections.newSetFromMap(new ConcurrentHashMap<BlaubotWifiP2PDevice, Boolean>());
this.wifiP2pManager = wifiP2pManager;
this.wifiP2pBeaconChannel = beaconChannel;
this.wifiP2pBroadcastReceiver = new BlaubotWifiP2PBroadcastReceiver(wifiP2pManager, beaconChannel);
this.wifiP2pBroadcastReceiver.addEventListener(wifiDirectEventListener);
this.bonjourListener = new BonjourListener();
this.upnpListener = new UPNPListener();
this.wifiP2pManager.setDnsSdResponseListeners(beaconChannel, bonjourListener, bonjourListener);
this.wifiP2pManager.setUpnpServiceResponseListener(wifiP2pBeaconChannel, upnpListener);
}
@Override
public Thread getAcceptThread() {
return ethernetBeaconAcceptThread;
}
@Override
public int getBeaconPort() {
return beaconPort;
}
@Override
public void close() throws IOException {
if (wifiP2pManager != null && wifiP2pBeaconChannel != null) {
wifiP2pManager.cancelConnect(wifiP2pBeaconChannel, null);
wifiP2pManager.clearServiceRequests(wifiP2pBeaconChannel, null);
wifiP2pManager.clearLocalServices(wifiP2pBeaconChannel, null);
wifiP2pManager.stopPeerDiscovery(wifiP2pBeaconChannel, null);
wifiP2pManager.removeGroup(wifiP2pBeaconChannel, null);
}
}
@Override
public void setCurrentContext(Context context) {
this.vibratorService = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
}
@Override
public void onResume(Activity context) {
}
@Override
public void onPause(Activity context) {
}
@Override
public void onNewIntent(Intent intent) {
}
/**
* Listener for upnp discovery events over WifiDirect
*/
private class UPNPListener implements WifiP2pManager.UpnpServiceResponseListener {
@Override
public void onUpnpServiceAvailable(List<String> uniqueServiceNames, WifiP2pDevice srcDevice) {
Log.d(LOG_TAG, "onUpnpServiceAvailable(" + uniqueServiceNames + ", " + srcDevice.deviceName + ")");
// check if one of the received record matches the beacon uuid
final String beaconUUidStr = beaconUuid.toString();
final String beaconUuidWithUPnPPrefix = "uuid:" + beaconUUidStr; // the beacon uuid
boolean deviceHasValidBeacon = false;
for (String uniqueServiceName : uniqueServiceNames) {
if (uniqueServiceName.startsWith(beaconUuidWithUPnPPrefix)) {
deviceHasValidBeacon = true;
break;
}
}
if (!deviceHasValidBeacon) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Received an UPnP record but was not our beacon uuid (" + beaconUUidStr + ") -> ignored.");
}
// abort, not relevant
return;
}
// TODO: NTH extract the uniqueDeviceId? How?
// we have found a device with a running blaubot instance, we store the device without the unique device id
// and try to make a state exchange by connecting to the device
final BlaubotWifiP2PDevice blaubotWifiP2PDevice = new UnidentifiedWifiP2pBlaubotDevice(srcDevice);
knownActiveDevices.add(blaubotWifiP2PDevice);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Added " + blaubotWifiP2PDevice + ", which has a valid BlaubotWifiP2PBeacon running, to the list of known devices.");
}
}
}
/**
* Listener for Bonjour discovery events over WifiDirect
*/
private class BonjourListener implements DnsSdServiceResponseListener, DnsSdTxtRecordListener {
@Override
public void onDnsSdTxtRecordAvailable(String fullDomainName, Map<String, String> txtRecordMap, final WifiP2pDevice srcDevice) {
Log.d(LOG_TAG, "onDnsSdTxtRecordAvailable(" + fullDomainName + ", " + txtRecordMap + ", " + srcDevice.deviceName + ")");
// check if the received record matches the beacon uuid
final String beaconUUidStr = beaconUuid.toString();
if (!fullDomainName.startsWith(beaconUUidStr)) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Received a DnsSdTxt record but was not our beacon uuid (" + beaconUUidStr + ") -> ignored: " + fullDomainName);
}
// abort, if irrelevant
return;
}
// extract the unique id
final String uniqueDeviceId = txtRecordMap.get(TXT_RECORD_UNIQUE_DEVICE_ID_KEY);
if (uniqueDeviceId == null) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Could not extract the uniqueDeviceId from the bonjour TXT record");
}
return;
}
// we have found a device with a running blaubot instance, we store the uniqueId to WifiP2pDevice
// mapping and try to make a state exchange by connecting to the device
final BlaubotWifiP2PDevice blaubotWifiP2PDevice = new BlaubotWifiP2PDevice(uniqueDeviceId, srcDevice);
knownActiveDevices.add(blaubotWifiP2PDevice);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Added " + blaubotWifiP2PDevice + ", which has a valid BlaubotWifiP2PBeacon running, to the list of known devices.");
}
}
@Override
public void onDnsSdServiceAvailable(String instanceName, String registrationType, WifiP2pDevice srcDevice) {
Log.d(LOG_TAG, "onDnsSdServiceAvailable(" + instanceName + ", " + registrationType + ", " + srcDevice + ")");
}
}
private class UnidentifiedWifiP2pBlaubotDevice extends BlaubotWifiP2PDevice implements IUnidentifiedBlaubotDevice {
public UnidentifiedWifiP2pBlaubotDevice(WifiP2pDevice device) {
super("UnidentifiedWifiP2pBlaubotDevice", device);
}
@Override
public void setUniqueDeviceId(String uniqueDeviceId) {
this.uniqueDeviceId = uniqueDeviceId;
}
}
private class UnidentifiedBlaubotDevice extends BlaubotDevice implements IUnidentifiedBlaubotDevice {
public UnidentifiedBlaubotDevice() {
super("UnidentifiedBlaubotDeviceFrom" + BlaubotWifiP2PBeacon.this);
}
@Override
public void setUniqueDeviceId(String uniqueDeviceId) {
this.uniqueDeviceId = uniqueDeviceId;
}
}
/**
* Only for logging
*/
private IBlaubotWifiDirectEventListener wifiDirectEventListener = new IBlaubotWifiDirectEventListener() {
@Override
public void onP2PWifiEnabled() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".WifiDirectEventListener", "onP2PWifiEnabled()");
}
}
@Override
public void onP2PWifiDisabled() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".WifiDirectEventListener", "onP2PWifiDisabled()");
}
}
@Override
public void onListOfPeersChanged(WifiP2pDeviceList deviceList) {
if (Log.logDebugMessages()) {
List<String> deviceNames = new ArrayList<>();
for (WifiP2pDevice device : deviceList.getDeviceList()) {
deviceNames.add(device.deviceName);
}
Log.d(LOG_TAG + ".WifiDirectEventListener", "onListOfPeersChanged(" + deviceNames + ")");
}
availablePeers = new ArrayList<>(deviceList.getDeviceList());
}
@Override
public void onDiscoveryStopped() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".WifiDirectEventListener", "onDiscoveryStopped()");
}
peerDiscoveryActive.set(false);
}
@Override
public void onDiscoveryStarted() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".WifiDirectEventListener", "onDiscoveryStarted()");
}
peerDiscoveryActive.set(true);
}
@Override
public void onConnectivityChanged(final WifiP2pInfo p2pInfo, final NetworkInfo networkInfo, final WifiP2pGroup group) {
if (Log.logDebugMessages()) {
// Log.d(LOG_TAG + ".WifiDirectEventListener", "onConnectivityChanged(" + p2pInfo + ", " + networkInfo + ", " + group + ")");
}
}
};
@Override
public IBlaubotAdapter getAdapter() {
return null;
}
@Override
public synchronized void startListening() {
if (isStarted) {
stopListening();
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Creating executor ...");
}
executorService = Executors.newSingleThreadExecutor();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting EthernetBeaconAcceptThread ...");
}
ethernetBeaconAcceptThread = new EthernetBeaconAcceptThread(incomingConnectionListener, this);
ethernetBeaconAcceptThread.start();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Starting WifiP2PBeaconScanner ...");
}
beaconScanner = new WifiP2PBeaconScanner();
beaconScanner.start();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Setting up search for beacon services ...");
}
// start advertising
executorService.execute(new Runnable() {
@Override
public void run() {
advertise();
}
});
// start discovery of other devices that are advertising
executorService.execute(new Runnable() {
@Override
public void run() {
createServiceRequestsAndStartDiscovery();
}
});
isStarted = true;
if (listeningStateListener != null) {
listeningStateListener.onListeningStarted(this);
}
}
@Override
public synchronized void stopListening() {
if (!isStarted()) {
return;
}
androidHardwareWorkaround();
disconnectPeer();
androidHardwareWorkaround();
wifiP2pManager.cancelConnect(wifiP2pBeaconChannel, null);
androidHardwareWorkaround();
wifiP2pManager.stopPeerDiscovery(wifiP2pBeaconChannel, null);
androidHardwareWorkaround();
wifiP2pManager.clearLocalServices(wifiP2pBeaconChannel, null);
androidHardwareWorkaround();
wifiP2pManager.clearServiceRequests(wifiP2pBeaconChannel, null);
// TODO: stop discovery and listening
if (beaconScanner != null && beaconScanner.isAlive()) {
Log.d(LOG_TAG, "Stopping WifiP2PBeaconScanner ...");
beaconScanner.interrupt();
try {
beaconScanner.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (executorService != null) {
Log.d(LOG_TAG, "Shutting down executor");
executorService.shutdown();
try {
while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) ;
} catch (InterruptedException e) {
} finally {
executorService = null;
}
}
wifiP2pManager.clearLocalServices(wifiP2pBeaconChannel, new ActionListener() {
@Override
public void onSuccess() {
Log.d(LOG_TAG, "P2P -> Cleared local services");
}
@Override
public void onFailure(int reason) {
Log.d(LOG_TAG, "P2P -> Failed to clear local services. Reason: " + actionListenerFailureReasonToString(reason));
}
});
if (ethernetBeaconAcceptThread != null && ethernetBeaconAcceptThread.isAlive()) {
try {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Waiting for beacon accept thread to finish ...");
}
ethernetBeaconAcceptThread.interrupt();
ethernetBeaconAcceptThread.join();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Beacon accept thread to finished ...");
}
} catch (InterruptedException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, e);
}
}
ethernetBeaconAcceptThread = null;
}
isStarted = false;
if (listeningStateListener != null) {
listeningStateListener.onListeningStopped(this);
}
}
@Override
public boolean isStarted() {
return this.isStarted;
}
@Override
public void setListeningStateListener(IBlaubotListeningStateListener stateListener) {
this.listeningStateListener = stateListener;
}
@Override
public void setAcceptorListener(IBlaubotIncomingConnectionListener acceptorListener) {
this.incomingConnectionListener = acceptorListener;
}
@Override
public ConnectionMetaDataDTO getConnectionMetaData() {
// TODO: maybe beacons should not derive from acceptors anymore
return null;
}
@Override
public void setDiscoveryEventListener(IBlaubotDiscoveryEventListener discoveryEventListener) {
this.discoveryEventListener = discoveryEventListener;
}
/**
* Creates a search request for the blaubot service to get notified via broadcast receivers
* and starts a discovery
*/
private void createServiceRequestsAndStartDiscovery() {
// bonjour
androidHardwareWorkaround();
final CountDownLatch addBonjourServiceRequestLatch = new CountDownLatch(1);
final WifiP2pDnsSdServiceRequest bonjourSearchRequest = WifiP2pDnsSdServiceRequest.newInstance(beaconUuid.toString(), BONJOUR_SERVICE_TYPE);
wifiP2pManager.addServiceRequest(wifiP2pBeaconChannel, bonjourSearchRequest, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Bonjour service search request added: " + bonjourSearchRequest);
}
addBonjourServiceRequestLatch.countDown();
}
@Override
public void onFailure(int reason) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to add bonjour service search request. Reason: " + actionListenerFailureReasonToString(reason));
}
addBonjourServiceRequestLatch.countDown();
}
});
try {
addBonjourServiceRequestLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// UPnP
androidHardwareWorkaround();
final CountDownLatch addUPNPServiceRequestLatch = new CountDownLatch(1);
final WifiP2pUpnpServiceRequest upnpServiceSearchRequest = WifiP2pUpnpServiceRequest.newInstance("ssdp:all");
wifiP2pManager.addServiceRequest(wifiP2pBeaconChannel, upnpServiceSearchRequest, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "UPnP service search request added: " + upnpServiceSearchRequest);
}
addUPNPServiceRequestLatch.countDown();
}
@Override
public void onFailure(int reason) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to add bonjour service search request. Reason: " + actionListenerFailureReasonToString(reason));
}
addBonjourServiceRequestLatch.countDown();
}
});
try {
addBonjourServiceRequestLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// after adding, start discovery
androidHardwareWorkaround();
final CountDownLatch startServiceDiscoveryLatch = new CountDownLatch(1);
wifiP2pManager.discoverServices(wifiP2pBeaconChannel, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Service discovery (UPnP & Bonjour) started");
}
startServiceDiscoveryLatch.countDown();
}
@Override
public void onFailure(int reason) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to start service discovery (UPnP & Bonjour), reason: " + actionListenerFailureReasonToString(reason));
}
startServiceDiscoveryLatch.countDown();
}
});
try {
startServiceDiscoveryLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* android hardware .... Many devices will not be ready in the onSuccess without time to do something (!!!)
* So we sleep a defined amount of time
*/
private void androidHardwareWorkaround() {
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Starts advertising our beacon over wifi direct service discovery
*/
private void advertise() {
// Add the bonjour local service
/**
* Attention:
* readTxtData(...) -> https://android.googlesource.com/platform/frameworks/base/+/cd92588/wifi/java/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceResponse.java
* as well as: Page 10 http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt
* => txt keys can only have 2 bytes; values 255 bytes!
* => The total size of a typical DNS-SD TXT record is intended to be small -- 200 bytes or less.
*
*/
Map<String, String> txtRecordsMap = new HashMap<>();
txtRecordsMap.put(TXT_RECORD_UNIQUE_DEVICE_ID_KEY, ownDevice.getUniqueDeviceID());
final WifiP2pServiceInfo bonjourServiceInfo = WifiP2pDnsSdServiceInfo.newInstance(beaconUuid.toString(), BONJOUR_SERVICE_TYPE, txtRecordsMap);
final CountDownLatch addLocalBonjourServiceLatch = new CountDownLatch(1);
final AtomicBoolean result = new AtomicBoolean(false);
androidHardwareWorkaround();
wifiP2pManager.addLocalService(wifiP2pBeaconChannel, bonjourServiceInfo, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Added Bojour-local service: " + bonjourServiceInfo);
}
result.set(true);
addLocalBonjourServiceLatch.countDown();
// we need to constantly discover peers to be visible (meh), see: https://code.google.com/p/android/issues/detail?id=37425
// so manager.discoverPeers() has to be called somewhere -> BeaconScanner does that
}
@Override
public void onFailure(int reason) {
if (Log.logDebugMessages()) {
Log.e(LOG_TAG, "Adding Bonjour-LocalService failed: " + actionListenerFailureReasonToString(reason));
}
addLocalBonjourServiceLatch.countDown();
}
});
try {
addLocalBonjourServiceLatch.await();
} catch (InterruptedException e) {
}
// now add the upnp local service
final CountDownLatch addLocalUpnpServiceLatch = new CountDownLatch(1);
final WifiP2pServiceInfo upnpServiceInfo = WifiP2pUpnpServiceInfo.newInstance(beaconUuid.toString(), UPNP_DEVICE_URN, Arrays.asList(UPNP_SERVICE_URN));
wifiP2pManager.addLocalService(wifiP2pBeaconChannel, upnpServiceInfo, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Added UPnP-local service: " + upnpServiceInfo);
}
result.set(true);
addLocalUpnpServiceLatch.countDown();
// we need to constantly discover peers to be visible (meh), see: https://code.google.com/p/android/issues/detail?id=37425
// so manager.discoverPeers() has to be called somewhere -> BeaconScanner does that
}
@Override
public void onFailure(int reason) {
if (Log.logDebugMessages()) {
Log.e(LOG_TAG, "Adding UPnP-LocalService failed: " + actionListenerFailureReasonToString(reason));
}
addLocalUpnpServiceLatch.countDown();
}
});
try {
addLocalUpnpServiceLatch.await();
} catch (InterruptedException e) {
}
}
@Override
public void onConnectionStateMachineStateChanged(IBlaubotState state) {
this.currentState = state;
}
@Override
public void setDiscoveryActivated(boolean active) {
this.discoveryActivated = active;
}
/**
* Semaphore preventing multiple beacon scanners to run concurrently.
*/
private final Semaphore beaconScannerSemaphore = new Semaphore(1);
/**
* Set by the broadcast receiver listeners to check, if the discovery is currently active
*/
private AtomicBoolean peerDiscoveryActive = new AtomicBoolean(false);
/**
* the last received list of peers
*/
private volatile ArrayList<WifiP2pDevice> availablePeers = new ArrayList<>();
/**
* TODO: implement properly
* The whole Bonjour and UPNP disvoery mechanisms just explore some possibly running Blaubot instances.
* This scanner runs through a list of discovered devices, filters out the already connected devices
* and creates WiFiP2p connections followed by socket connections to these devices sequentially to
* do the state exchange using the generalized tasks.
*/
class WifiP2PBeaconScanner extends Thread {
private String LOG_TAG = "BlaubotWifiP2PBeacon.WifiP2PBeaconScanner";
/**
* Sleep time (ms) between probes to other devices
*/
public static final long PER_DEVICE_TIMEOUT = 5000;
/**
* Creates the list of devices to be scanned.
*
* @return list of devices to scan without connected kingdom devices and ordered descending by uniqueDeviceId
*/
private ArrayList<BlaubotWifiP2PDevice> getDevicesToScan() {
ArrayList<BlaubotWifiP2PDevice> devicesToScan = new ArrayList<BlaubotWifiP2PDevice>();
if(CONNECT_TO_ALL_AVAILABLE_PEERS) {
// all known peers will be scanned - there are quite some dangers here, so warn the user
Log.w(LOG_TAG, "CONNECT_TO_ALL_AVAILABLE_PEERS is activated! Strange things can happen, I hope you know what you're doing.");
for(WifiP2pDevice d : availablePeers) {
UnidentifiedWifiP2pBlaubotDevice blaubotDevice = new UnidentifiedWifiP2pBlaubotDevice(d);
blaubotDevice.setUniqueDeviceId("UnidentifiedWifiP2pBlaubotDevice for P2pDevice " + d.deviceName);
devicesToScan.add(blaubotDevice);
}
}
// We only check devices that are bonded and not already connected to our network to minimize
// the expensive lookups and connectivity traffic.
Set<String> blaubotNetworkDevices = kingdomCensusLifeCycleListener.getConnectedUniqueIds();
for (BlaubotWifiP2PDevice d : knownActiveDevices) {
// filter connected devices
if (!blaubotNetworkDevices.contains(d.getUniqueDeviceID())) {
devicesToScan.add(d);
}
}
// After filtering we sort the devices descending by their unique id
Collections.sort(devicesToScan);
Collections.reverse(devicesToScan);
return devicesToScan;
}
/**
* This latch awaits a connect of the wifi direct p2p connection to another device.
* Our happy path lifecycle is as follows:
* - BeaconScanner starts a WifiDirect P2P group connect to the other device
* - The connect method adds the device and it's uniqueDeviceId to the connectingDevices map, so that we know in the broadcast receiver listener, which events are interesting for us
* - From the connect method, we know if the connect attempt was started or failed before that
* - If started, this latch is counted down on receive of an event on the wifiDirectListener of the scanner
* - If not, the latch is immediately counted down based on the negative result of connect()
* - The beacon scanner then awaits this latch, which will be counted down on a disconnect event received for this device on the wifiDirectListener
*/
private volatile CountDownLatch groupChangeLatch = new CountDownLatch(0); // initially set to avoid possible nullpointers on startup
private volatile boolean interrupted = false;
@Override
public void interrupt() {
super.interrupt();
interrupted = true;
}
@Override
public void run() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "BeaconScanner started.");
}
try {
beaconScannerSemaphore.acquire();
} catch (InterruptedException e) {
return;
}
// add listener
wifiP2pBroadcastReceiver.addEventListener(wifiDirectListener);
// start service discovery to get informend on new devices and start peer discovery to be visible
wifiP2pManager.discoverPeers(wifiP2pBeaconChannel, null);
while (!isInterrupted() && beaconScanner == Thread.currentThread() && !interrupted) {
final ArrayList<BlaubotWifiP2PDevice> devicesToScan = getDevicesToScan();
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Scanning devices: " + devicesToScan);
}
// we need have peer discovery running all the time -> http://stackoverflow.com/a/23850036/1142790
if (!peerDiscoveryActive.get()) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "PeerDiscovery is not active, activating.");
}
final CountDownLatch latch = new CountDownLatch(1);
executorService.execute(new Runnable() {
@Override
public void run() {
androidHardwareWorkaround();
wifiP2pManager.discoverPeers(wifiP2pBeaconChannel, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "(re)started peer discovery");
}
latch.countDown();
}
@Override
public void onFailure(int reason) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Failed to (re)start peer discovery, reason: " + actionListenerFailureReasonToString(reason));
}
latch.countDown();
}
});
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if ((devicesToScan.isEmpty() || availablePeers.isEmpty() || !discoveryActivated)) {
// Do not connect to peers, if there are no peers available, no service records were found or the connection is explicitly deactivated.
// also, if we have devices in deviceToScan (which are discovered over bonjour, but no availablePeers, the connection will fail:
Log.w(LOG_TAG, "Not connecting to peers. BeaconDiscoveryActivated: " + discoveryActivated + ", Available peers: " + availablePeers.size() + ", PeersFoundByBonjourOrUPnP: " + knownActiveDevices.size() + ", BonjourPeersAfterFilter: " + devicesToScan + ", PeerDiscoveryActive: " + peerDiscoveryActive.get());
if (!discoveryActivated) {
wifiP2pManager.discoverServices(wifiP2pBeaconChannel, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "(Re)initiated service discovery");
}
}
@Override
public void onFailure(int reason) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Could not (re)initiate service discovery, reason: " + actionListenerFailureReasonToString(reason));
}
}
});
}
} else {
Deque<BlaubotWifiP2PDevice> scanQueue = new ArrayDeque<>(getDevicesToScan());
while (!scanQueue.isEmpty()) {
final BlaubotWifiP2PDevice device = scanQueue.poll();
// 1. create wifi p2p connection
// 2. create tcp connection
// 3. do the state exchange
final String uniqueDeviceID = device.getUniqueDeviceID();
final WifiP2pDevice wifiP2pDevice = device.getWifiP2pDevice();
// ensure that there is no group formed at the time of connect
final AtomicReference<WifiP2pGroup> groupInfo = new AtomicReference<>();
int i = 0;
do {
if(i++>0) {
if(Log.logDebugMessages()) {
Log.d(LOG_TAG, "Still have a group (" + groupInfo.get().getNetworkName() + "). Will wait until our channel is free again.");
}
}
final CountDownLatch latch = new CountDownLatch(1);
androidHardwareWorkaround();
wifiP2pManager.requestGroupInfo(wifiP2pBeaconChannel, new WifiP2pManager.GroupInfoListener() {
@Override
public void onGroupInfoAvailable(WifiP2pGroup group) {
groupInfo.set(group);
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (!(groupInfo.get() == null));
if (groupInfo.get() != null) {
Log.w(LOG_TAG, "group status: " + groupInfo.get());
continue;
}
// connect wifi direct
androidHardwareWorkaround();
ConnectInitiationResult connectResult = connectToPeer(uniqueDeviceID, wifiP2pDevice);
if (connectResult != ConnectInitiationResult.INITIATED) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "WifiP2pConnection to device " + device + " failed.");
}
} else if(connectResult == ConnectInitiationResult.BUSY) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "The wifi direct adapter was too busy to connect. Going to sleep for some time and retrying then.");
try {
// put the device back to the queue's head
scanQueue.addFirst(device);
// but sleep a little longer than usual
Thread.sleep(PER_DEVICE_TIMEOUT*2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Awaiting connectivity callback from android's wifi p2p broadcast receiver");
}
}
// tcp connection and state exchagne will happen in onConnectivityChanged of this.wifiP2pConnectionListener
try {
Thread.sleep(PER_DEVICE_TIMEOUT);
} catch (InterruptedException e) {
interrupted = true;
break;
}
}
}
try {
Thread.sleep(PER_DEVICE_TIMEOUT);
} catch (InterruptedException e) {
interrupted = true;
break;
}
}
// remove listener
wifiP2pBroadcastReceiver.removeEventListener(wifiDirectListener);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "BeaconScanner finished.");
}
beaconScannerSemaphore.release();
}
/**
* A map containing the devices we are connecting to at the moment and their uniqueDeviceIds
*/
private Map<WifiP2pDevice, String> connectingDevices = new ConcurrentHashMap();
/**
* Is added on scanner start and removed on scanner end.
* Listens to connected groups.
* If the client list of a formed group contains one of the devices in this.connectingDevices,
* the listener tries to open a socket connection for a state exchange with the other beacon's
* accept thread.
*/
private IBlaubotWifiDirectEventListener wifiDirectListener = new BlaubotWifiDirectEventListenerAdapter() {
@Override
public void onConnectivityChanged(final WifiP2pInfo p2pInfo, NetworkInfo networkInfo, WifiP2pGroup group) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "onConnectivityChanged: " + p2pInfo + "; " + networkInfo + "; " + group);
}
if (!networkInfo.isConnectedOrConnecting() && networkInfo.getState() == NetworkInfo.State.DISCONNECTED) {
Log.w(LOG_TAG, "State is not connceted or connecting. ignoring onConnectivityChanged event");
}
if (p2pInfo.groupFormed) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "The group was formed");
}
if(vibratorService != null) {
// notify with haptic feedback
vibratorService.vibrate(350);
}
if (!p2pInfo.isGroupOwner) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Other side is the owner, so we have to connect to " + p2pInfo.groupOwnerAddress + ":" + beaconPort);
}
boolean ownerIsInConnectingDevices = connectingDevices.keySet().contains(group.getOwner());
if (ownerIsInConnectingDevices) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "The owner is in the connecting devices list");
}
}
final NetworkInfo.State networkState = networkInfo.getState();
if (networkState != NetworkInfo.State.CONNECTED) {
throw new IllegalStateException();
}
executorService.execute(new Runnable() {
@Override
public void run() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Initiating beacon state exchange via ethernet socket ...");
}
androidHardwareWorkaround();
IUnidentifiedBlaubotDevice device = new UnidentifiedBlaubotDevice();
// receive the group owners ip and connect to the beacon, then disconnect
final InetAddress groupOwnerAddress = p2pInfo.groupOwnerAddress;
// try to connect, then exchange states via tcp/ip
Socket clientSocket = null;
try {
clientSocket = new Socket(groupOwnerAddress, beaconPort);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "Connecting to " + clientSocket + " for Beacon messaging");
}
BlaubotEthernetUtils.sendOwnUniqueIdThroughSocket(ownDevice, clientSocket);
BlaubotEthernetConnection connection = new BlaubotEthernetConnection(device, clientSocket);
final List<ConnectionMetaDataDTO> ownAcceptorsMetaDataList = BlaubotAdapterHelper.getConnectionMetaDataList(BlaubotAdapterHelper.getConnectionAcceptors(blaubot.getAdapters()));
ExchangeStatesTask exchangeStatesTask = new ExchangeStatesTask(ownDevice, connection, currentState, ownAcceptorsMetaDataList, beaconStore, discoveryEventListener);
exchangeStatesTask.run();
} catch (IOException e) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Connection to " + device + "'s beacon failed: " + e.getMessage());
}
} finally {
// close socket and connection
if (clientSocket != null) {
try {
clientSocket.shutdownInput();
clientSocket.shutdownOutput();
} catch (IOException e) {
// don't care
}
try {
clientSocket.close();
} catch (IOException e) {
// couldn't care less
}
}
// finally wait some time for the android hardware, then disconnect
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
disconnectPeer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
});
} else {
final Collection<WifiP2pDevice> clientList;
if (group != null) {
clientList = group.getClientList();
} else {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG, "Inconsitent state from android event! groupFormed is true but no group was given (null)");
}
clientList = new ArrayList<>();
}
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "We are group owner. The other side has to connect. Awaiting message or timeout ... Group client list: " + clientList);
}
Set<WifiP2pDevice> clientSet = new HashSet<>(clientList);
clientSet.retainAll(connectingDevices.keySet());
Log.w(LOG_TAG, "Devices that are connected and interesting for us: " + clientSet);
// -- we are group owner and therefore have to wait for the other side to connect
// since this is a bi-directional conversation, we don't actually need to contact
// the other side and can live with the information they will provide for us so
// we don't bother to connect to the non-group owner.
// the other side will close the connection after a state exchange
// TODO: theoretically we have to disconnect the remote connection in cases it is harmful by not creating a socket to communicate and closing the group. But that is out of scope for now
}
} else {
// group could not be formed
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "Group was not formed");
}
}
}
};
/**
* Initiates a connection to the given srcDevice via WifiDirect
*
* @param uniqueDeviceId the uniqueDeviceId found via bonjour
* @param srcDevice the device info
* @return INITIATED, if successful
*/
private ConnectInitiationResult connectToPeer(String uniqueDeviceId, final WifiP2pDevice srcDevice) {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicBoolean result = new AtomicBoolean(false);
final AtomicInteger errorResult = new AtomicInteger(0);
connectingDevices.put(srcDevice, uniqueDeviceId);
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = srcDevice.deviceAddress;
if (Log.logDebugMessages())
Log.d(LOG_TAG, "connecting to " + srcDevice.deviceName + " ...");
wifiP2pManager.connect(wifiP2pBeaconChannel, config, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG, "connect.onSuccess() - we initiated the connect process to " + srcDevice.deviceName);
}
result.set(true);
latch.countDown();
}
@Override
public void onFailure(int reason) {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG, "connect.onFailure(" + reason + ") Failed to connect to " + srcDevice.deviceName + ". Reason: " + actionListenerFailureReasonToString(reason));
}
errorResult.set(reason);
result.set(false);
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (result.get()) {
return ConnectInitiationResult.INITIATED;
} else {
final int errorCode = errorResult.get();
if (errorCode == WifiP2pManager.ERROR)
return ConnectInitiationResult.ERROR;
else if (errorCode == WifiP2pManager.BUSY)
return ConnectInitiationResult.BUSY;
else if (errorCode == WifiP2pManager.NO_SERVICE_REQUESTS)
return ConnectInitiationResult.NO_SERVICE_REQUESTS;
else if (errorCode == WifiP2pManager.P2P_UNSUPPORTED)
return ConnectInitiationResult.P2P_UNSUPPORTED;
else
return ConnectInitiationResult.UNKNOWN;
}
}
}
/**
* Corresponds to the return codes of connectToPeer
*/
private enum ConnectInitiationResult {
/**
* If the connection was successfully initiated
*/
INITIATED,
ERROR,
BUSY,
P2P_UNSUPPORTED,
NO_SERVICE_REQUESTS,
UNKNOWN;
}
/**
* Disconnects the current peer connection which is uniquely bound to the (single) channel used
* by the beacon.
*
* @return true iff removing the peer succeeded
*/
private boolean disconnectPeer() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".disconnectPeer()", "Disconnecting from peer ...");
}
final AtomicBoolean out = new AtomicBoolean(false);
if (wifiP2pManager != null && wifiP2pBeaconChannel != null) {
final CountDownLatch latch = new CountDownLatch(1);
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".disconnectPeer()", "Requesting group info on channel " + wifiP2pBeaconChannel);
}
wifiP2pManager.requestGroupInfo(wifiP2pBeaconChannel, new WifiP2pManager.GroupInfoListener() {
@Override
public void onGroupInfoAvailable(WifiP2pGroup group) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".disconnectPeer()", "Got group info to disconnect from peer: " + group);
}
if (group != null) {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".disconnectPeer()", "Removing group to disconnect from peer. Group: " + group);
}
wifiP2pManager.removeGroup(wifiP2pBeaconChannel, new ActionListener() {
@Override
public void onSuccess() {
if (Log.logDebugMessages()) {
Log.d(LOG_TAG + ".disconnectPeer()", "removeGroup onSuccess");
}
out.set(true);
latch.countDown();
}
@Override
public void onFailure(int reason) {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG + ".disconnectPeer()", "removeGroup onFailure -" + reason);
}
out.set(false);
latch.countDown();
}
});
} else {
if (Log.logWarningMessages()) {
Log.w(LOG_TAG + ".disconnectPeer()", "Got no group info while trying to disconnect (group was null, should not be a problem)");
}
out.set(false);
latch.countDown();
}
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
if (Log.logErrorMessages()) {
Log.e(LOG_TAG + ".disconnectPeer()", "Could not disconnect from peer: Either wifiP2pManager or channel was null");
}
}
return out.get();
}
@Override
public void setBlaubot(Blaubot blaubot) {
this.blaubot = blaubot;
this.beaconUuid = blaubot.getUuidSet().getBeaconUUID();
this.ownDevice = blaubot.getOwnDevice();
this.kingdomCensusLifeCycleListener = new KingdomCensusLifecycleListener(ownDevice);
this.blaubot.addLifecycleListener(kingdomCensusLifeCycleListener);
}
@Override
public void setBeaconStore(IBlaubotBeaconStore beaconStore) {
this.beaconStore = beaconStore;
}
/**
* The {@link android.net.wifi.p2p.WifiP2pManager.ActionListener}s return error codes for their
* fail callbacks. This method maps these to human readable strings for logging purposes.
*
* @param reason the reason code
* @return the human readable string
*/
private static String actionListenerFailureReasonToString(int reason) {
if (reason == WifiP2pManager.ERROR) {
return "ERROR";
}
if (reason == WifiP2pManager.P2P_UNSUPPORTED) {
return "P2P_UNSUPPORTED";
}
if (reason == WifiP2pManager.BUSY) {
return "BUSY";
}
if (reason == WifiP2pManager.NO_SERVICE_REQUESTS) {
return "NO_SERVICE_REQUESTS";
}
return "UNKNOWN (" + reason + ")";
}
@Override
public BroadcastReceiver getReceiver() {
return wifiP2pBroadcastReceiver;
}
@Override
public IntentFilter getIntentFilter() {
return BlaubotWifiP2PBroadcastReceiver.createWifiP2PIntentFilter();
}
}