package org.commcare.android.nsd;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Build;
import android.util.Log;
import org.commcare.logging.AndroidLogger;
import org.commcare.utils.TimeBoundOperation;
import org.javarosa.core.services.Logger;
import java.net.InetAddress;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
/**
* Manages relevant hooks for communicating with devices on the local network
* like the CommCareHub app. Provides a place for registering listeners independent
* from the app lifecycle and translating that into actionable hooks.
*
* Created by ctsims on 2/19/2016.
*/
public class NSDDiscoveryTools {
private static final String TAG = NSDDiscoveryTools.class.getSimpleName();
private static final String SERVICE_TYPE = "_http._tcp.";
private static final String SERVICE_NAME = "commcare_micronode";
private final static HashMap<String, MicroNode> mAttachedMicronodes = new HashMap<>();
private final static Set<NsdServiceListener> listeners = new HashSet<>();
private final static Object nsdToolsLock = new Object();
private static NsdManager.DiscoveryListener mDiscoveryListener;
private static NsdManager mNsdManager;
public enum NsdState {
Init,
Discovery,
Idle,
Unsupported
}
private static NsdState state = NsdState.Init;
public static void registerForNsdServices(Context context, NsdServiceListener listener) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
addListener(listener);
doDiscovery(context);
}
public static void unregisterForNsdServices(NsdServiceListener listener) {
removeListener(listener);
}
private static void addListener(final NsdServiceListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
if (state != NsdState.Init) {
if (mAttachedMicronodes.size() > 0) {
//Receivers should expect to receive these messages from not-their-main thread
//which is managed inherently during discovery, but won't be during
//registration
new Thread(new Runnable() {
@Override
public void run() {
listener.onMicronodeDiscovery();
}
}).start();
}
}
}
public static Collection<MicroNode> getAvailableMicronodes() {
//Make a shallow copy in case this list is modified, since it is likely
//to have long operaitons run during iteration.
return new HashSet<>(mAttachedMicronodes.values());
}
private static void removeListener(NsdServiceListener listener) {
synchronized (listeners) {
if (listeners.contains(listener)) {
listeners.remove(listener);
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static void doDiscovery(Context context) {
synchronized (nsdToolsLock) {
if (mNsdManager == null) {
if(!connectNsdManager(context)) {
return;
}
}
if (state == NsdState.Init || state == NsdState.Idle) {
initializeDiscoveryListener();
state = NsdState.Discovery;
mNsdManager.discoverServices(SERVICE_TYPE,
NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static boolean connectNsdManager(final Context context) {
synchronized (nsdToolsLock) {
//sometimes the service fetch basically times out forever, thanks for the clear
//workaround steps google. Only give this half a second to fire, and then ignore the
//service as broken.
//Apparently NSDManager is a disaster before Android 5 and we should consider using
//a different NSD stack. Ugh.
//https://code.google.com/p/android/issues/detail?id=70778
boolean connected = new TimeBoundOperation(500) {
NsdManager manager;
@Override
public void run() {
manager = (NsdManager)context.getSystemService(Context.NSD_SERVICE);
}
@Override
public void commit() {
mNsdManager = manager;
}
}.execute();
if(!connected) {
Logger.log(AndroidLogger.TYPE_MAINTENANCE, "NSD Service Failed to connect");
}
if(mNsdManager == null) {
Log.d(TAG, "NSD Service Unavailable on device");
state = NsdState.Unsupported;
}
return connected;
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static void initializeDiscoveryListener() {
// Instantiate a new DiscoveryListener
mDiscoveryListener = new NsdManager.DiscoveryListener() {
// Called as soon as service discovery begins.
@Override
public void onDiscoveryStarted(String regType) {
Log.d(TAG, "Service discovery started");
}
@Override
public void onServiceFound(NsdServiceInfo service) {
// A service was found! Do something with it.
Log.d(TAG, "Service discovery success" + service);
if (!service.getServiceType().equals(SERVICE_TYPE)) {
// Service type is the string containing the protocol and
// transport layer for this service.
Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
} else if (service.getServiceName().equals(SERVICE_NAME)) {
Log.d(TAG, "Found CommCare Micronode");
mNsdManager.resolveService(service, getResolveListener());
}
}
@Override
public void onServiceLost(NsdServiceInfo service) {
// When the network service is no longer available.
// Internal bookkeeping code goes here.
Log.e(TAG, "service lost" + service);
}
@Override
public void onDiscoveryStopped(String serviceType) {
Log.i(TAG, "Discovery stopped: " + serviceType);
state = NsdState.Idle;
}
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
mNsdManager.stopServiceDiscovery(this);
state = NsdState.Idle;
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
mNsdManager.stopServiceDiscovery(this);
}
};
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static NsdManager.ResolveListener getResolveListener() {
return new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Called when the resolve fails. Use the error code to debug.
Log.e(TAG, "Resolve failed" + errorCode);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
Log.e(TAG, "Resolve Succeeded. " + serviceInfo);
int port = serviceInfo.getPort();
InetAddress host = serviceInfo.getHost();
attachMicronode(host, port);
}
};
}
private static void updateAttachedServices() {
for (NsdServiceListener listener : listeners) {
listener.onMicronodeDiscovery();
}
}
private static void attachMicronode(InetAddress host, int port) {
String nodeAddress = "http://" + host.getHostAddress() + ":" + port;
if (mAttachedMicronodes.containsKey(nodeAddress)) {
//already know about this one, nothing to be done
return;
}
mAttachedMicronodes.put(nodeAddress, new MicroNode(nodeAddress));
updateAttachedServices();
}
}