diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 05682412..b301979c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,11 +5,15 @@ - + + + diff --git a/app/src/main/java/com/limelight/discovery/DiscoveryService.java b/app/src/main/java/com/limelight/discovery/DiscoveryService.java index 07644ac1..f8c855d6 100644 --- a/app/src/main/java/com/limelight/discovery/DiscoveryService.java +++ b/app/src/main/java/com/limelight/discovery/DiscoveryService.java @@ -3,22 +3,21 @@ package com.limelight.discovery; import java.util.List; import com.limelight.nvstream.mdns.MdnsComputer; +import com.limelight.nvstream.mdns.JmDNSDiscoveryAgent; import com.limelight.nvstream.mdns.MdnsDiscoveryAgent; import com.limelight.nvstream.mdns.MdnsDiscoveryListener; +import com.limelight.nvstream.mdns.NsdManagerDiscoveryAgent; import android.app.Service; -import android.content.Context; import android.content.Intent; -import android.net.wifi.WifiManager; -import android.net.wifi.WifiManager.MulticastLock; import android.os.Binder; +import android.os.Build; import android.os.IBinder; public class DiscoveryService extends Service { private MdnsDiscoveryAgent discoveryAgent; private MdnsDiscoveryListener boundListener; - private MulticastLock multicastLock; public class DiscoveryBinder extends Binder { public void setListener(MdnsDiscoveryListener listener) { @@ -26,13 +25,11 @@ public class DiscoveryService extends Service { } public void startDiscovery(int queryIntervalMs) { - multicastLock.acquire(); discoveryAgent.startDiscovery(queryIntervalMs); } public void stopDiscovery() { discoveryAgent.stopDiscovery(); - multicastLock.release(); } public List getComputerSet() { @@ -42,11 +39,7 @@ public class DiscoveryService extends Service { @Override public void onCreate() { - WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); - multicastLock = wifiMgr.createMulticastLock("Limelight mDNS"); - multicastLock.setReferenceCounted(false); - - discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() { + MdnsDiscoveryListener listener = new MdnsDiscoveryListener() { @Override public void notifyComputerAdded(MdnsComputer computer) { if (boundListener != null) { @@ -60,7 +53,22 @@ public class DiscoveryService extends Service { boundListener.notifyDiscoveryFailure(e); } } - }); + }; + + // Prior to Android 14, NsdManager doesn't provide all the capabilities needed for parity + // with jmDNS (specifically handling multiple addresses for a single service). There are + // also documented reliability bugs early in the Android 4.x series shortly after it was + // introduced. The benefit of using NsdManager over jmDNS is that it works correctly in + // environments where mDNS proxying is required, like ChromeOS, WSA, and the emulator. + // + // As such, we use the jmDNS-based MdnsDiscoveryAgent prior to Android 14 and NsdManager + // on Android 14 and above. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + discoveryAgent = new JmDNSDiscoveryAgent(getApplicationContext(), listener); + } + else { + discoveryAgent = new NsdManagerDiscoveryAgent(getApplicationContext(), listener); + } } private final DiscoveryBinder binder = new DiscoveryBinder(); @@ -74,7 +82,6 @@ public class DiscoveryService extends Service { public boolean onUnbind(Intent intent) { // Stop any discovery session discoveryAgent.stopDiscovery(); - multicastLock.release(); // Unbind the listener boundListener = null; diff --git a/app/src/main/java/com/limelight/nvstream/mdns/JmDNSDiscoveryAgent.java b/app/src/main/java/com/limelight/nvstream/mdns/JmDNSDiscoveryAgent.java new file mode 100644 index 00000000..466304cf --- /dev/null +++ b/app/src/main/java/com/limelight/nvstream/mdns/JmDNSDiscoveryAgent.java @@ -0,0 +1,269 @@ +package com.limelight.nvstream.mdns; + +import android.content.Context; +import android.net.wifi.WifiManager; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.ArrayList; +import java.util.HashSet; + +import javax.jmdns.JmmDNS; +import javax.jmdns.NetworkTopologyDiscovery; +import javax.jmdns.ServiceEvent; +import javax.jmdns.ServiceInfo; +import javax.jmdns.ServiceListener; +import javax.jmdns.impl.NetworkTopologyDiscoveryImpl; + +import com.limelight.LimeLog; + +public class JmDNSDiscoveryAgent extends MdnsDiscoveryAgent implements ServiceListener { + private static final String SERVICE_TYPE = "_nvstream._tcp.local."; + private WifiManager.MulticastLock multicastLock; + private Thread discoveryThread; + private HashSet pendingResolution = new HashSet<>(); + + // The resolver factory's instance member has a static lifetime which + // means our ref count and listener must be static also. + private static int resolverRefCount = 0; + private static HashSet listeners = new HashSet<>(); + private static ServiceListener nvstreamListener = new ServiceListener() { + @Override + public void serviceAdded(ServiceEvent event) { + HashSet localListeners; + + // Copy the listener set into a new set so we can invoke + // the callbacks without holding the listeners monitor the + // whole time. + synchronized (listeners) { + localListeners = new HashSet(listeners); + } + + for (ServiceListener listener : localListeners) { + listener.serviceAdded(event); + } + } + + @Override + public void serviceRemoved(ServiceEvent event) { + HashSet localListeners; + + // Copy the listener set into a new set so we can invoke + // the callbacks without holding the listeners monitor the + // whole time. + synchronized (listeners) { + localListeners = new HashSet(listeners); + } + + for (ServiceListener listener : localListeners) { + listener.serviceRemoved(event); + } + } + + @Override + public void serviceResolved(ServiceEvent event) { + HashSet localListeners; + + // Copy the listener set into a new set so we can invoke + // the callbacks without holding the listeners monitor the + // whole time. + synchronized (listeners) { + localListeners = new HashSet(listeners); + } + + for (ServiceListener listener : localListeners) { + listener.serviceResolved(event); + } + } + }; + + public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl { + @Override + public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) { + // This is an copy of jmDNS's implementation, except we omit the multicast check, since + // it seems at least some devices lie about interfaces not supporting multicast when they really do. + try { + if (!networkInterface.isUp()) { + return false; + } + + /* + if (!networkInterface.supportsMulticast()) { + return false; + } + */ + + if (networkInterface.isLoopback()) { + return false; + } + + return true; + } catch (Exception exception) { + return false; + } + } + } + + static { + // Override jmDNS's default topology discovery class with ours + NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() { + @Override + public NetworkTopologyDiscovery newNetworkTopologyDiscovery() { + return new MyNetworkTopologyDiscovery(); + } + }); + } + + private static JmmDNS referenceResolver() { + synchronized (JmDNSDiscoveryAgent.class) { + JmmDNS instance = JmmDNS.Factory.getInstance(); + if (++resolverRefCount == 1) { + // This will cause the listener to be invoked for known hosts immediately. + // JmDNS only supports one listener per service, so we have to do this here + // with a static listener. + instance.addServiceListener(SERVICE_TYPE, nvstreamListener); + } + return instance; + } + } + + private static void dereferenceResolver() { + synchronized (JmDNSDiscoveryAgent.class) { + if (--resolverRefCount == 0) { + try { + JmmDNS.Factory.close(); + } catch (IOException e) {} + } + } + } + + public JmDNSDiscoveryAgent(Context context, MdnsDiscoveryListener listener) { + super(listener); + + // Create the multicast lock required to receive mDNS traffic + WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + multicastLock = wifiMgr.createMulticastLock("Limelight mDNS"); + multicastLock.setReferenceCounted(false); + } + + private void handleResolvedServiceInfo(ServiceInfo info) { + synchronized (pendingResolution) { + pendingResolution.remove(info.getName()); + } + + try { + handleServiceInfo(info); + } catch (UnsupportedEncodingException e) { + // Invalid DNS response + LimeLog.info("mDNS: Invalid response for machine: "+info.getName()); + return; + } + } + + private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException { + reportNewComputer(info.getName(), info.getPort(), info.getInet4Addresses(), info.getInet6Addresses()); + } + + public void startDiscovery(final int discoveryIntervalMs) { + // Kill any existing discovery before starting a new one + stopDiscovery(); + + // Acquire the multicast lock to start receiving mDNS traffic + multicastLock.acquire(); + + // Add our listener to the set + synchronized (listeners) { + listeners.add(JmDNSDiscoveryAgent.this); + } + + discoveryThread = new Thread() { + @Override + public void run() { + // This may result in listener callbacks so we must register + // our listener first. + JmmDNS resolver = referenceResolver(); + + try { + while (!Thread.interrupted()) { + // Start an mDNS request + resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs); + + // Run service resolution again for pending machines + ArrayList pendingNames; + synchronized (pendingResolution) { + pendingNames = new ArrayList(pendingResolution); + } + for (String name : pendingNames) { + LimeLog.info("mDNS: Retrying service resolution for machine: "+name); + ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500); + if (infos != null && infos.length != 0) { + LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries"); + for (ServiceInfo svcinfo : infos) { + handleResolvedServiceInfo(svcinfo); + } + } + } + + // Wait for the next polling interval + try { + Thread.sleep(discoveryIntervalMs); + } catch (InterruptedException e) { + break; + } + } + } + finally { + // Dereference the resolver + dereferenceResolver(); + } + } + }; + discoveryThread.setName("mDNS Discovery Thread"); + discoveryThread.start(); + } + + public void stopDiscovery() { + // Release the multicast lock to stop receiving mDNS traffic + multicastLock.release(); + + // Remove our listener from the set + synchronized (listeners) { + listeners.remove(JmDNSDiscoveryAgent.this); + } + + // If there's already a running thread, interrupt it + if (discoveryThread != null) { + discoveryThread.interrupt(); + discoveryThread = null; + } + } + + @Override + public void serviceAdded(ServiceEvent event) { + LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName()); + + ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500); + if (info == null) { + // This machine is pending resolution + synchronized (pendingResolution) { + pendingResolution.add(event.getInfo().getName()); + } + return; + } + + LimeLog.info("mDNS: Resolved (blocking)"); + handleResolvedServiceInfo(info); + } + + @Override + public void serviceRemoved(ServiceEvent event) { + LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName()); + } + + @Override + public void serviceResolved(ServiceEvent event) { + // We handle this synchronously + } +} diff --git a/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java b/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java index b242297a..661578b6 100644 --- a/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java +++ b/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java @@ -1,165 +1,64 @@ package com.limelight.nvstream.mdns; -import java.io.IOException; -import java.io.UnsupportedEncodingException; +import com.limelight.LimeLog; + import java.net.Inet4Address; import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.NetworkInterface; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import javax.jmdns.JmmDNS; -import javax.jmdns.NetworkTopologyDiscovery; -import javax.jmdns.ServiceEvent; -import javax.jmdns.ServiceInfo; -import javax.jmdns.ServiceListener; -import javax.jmdns.impl.NetworkTopologyDiscoveryImpl; +public abstract class MdnsDiscoveryAgent { + protected MdnsDiscoveryListener listener; -import com.limelight.LimeLog; - -public class MdnsDiscoveryAgent implements ServiceListener { - public static final String SERVICE_TYPE = "_nvstream._tcp.local."; - - private MdnsDiscoveryListener listener; - private Thread discoveryThread; - private HashSet computers = new HashSet<>(); - private HashSet pendingResolution = new HashSet<>(); - - // The resolver factory's instance member has a static lifetime which - // means our ref count and listener must be static also. - private static int resolverRefCount = 0; - private static HashSet listeners = new HashSet<>(); - private static ServiceListener nvstreamListener = new ServiceListener() { - @Override - public void serviceAdded(ServiceEvent event) { - HashSet localListeners; - - // Copy the listener set into a new set so we can invoke - // the callbacks without holding the listeners monitor the - // whole time. - synchronized (listeners) { - localListeners = new HashSet(listeners); - } - - for (ServiceListener listener : localListeners) { - listener.serviceAdded(event); - } - } - - @Override - public void serviceRemoved(ServiceEvent event) { - HashSet localListeners; - - // Copy the listener set into a new set so we can invoke - // the callbacks without holding the listeners monitor the - // whole time. - synchronized (listeners) { - localListeners = new HashSet(listeners); - } - - for (ServiceListener listener : localListeners) { - listener.serviceRemoved(event); - } - } - - @Override - public void serviceResolved(ServiceEvent event) { - HashSet localListeners; - - // Copy the listener set into a new set so we can invoke - // the callbacks without holding the listeners monitor the - // whole time. - synchronized (listeners) { - localListeners = new HashSet(listeners); - } - - for (ServiceListener listener : localListeners) { - listener.serviceResolved(event); - } - } - }; - - public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl { - @Override - public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) { - // This is an copy of jmDNS's implementation, except we omit the multicast check, since - // it seems at least some devices lie about interfaces not supporting multicast when they really do. - try { - if (!networkInterface.isUp()) { - return false; - } - - /* - if (!networkInterface.supportsMulticast()) { - return false; - } - */ - - if (networkInterface.isLoopback()) { - return false; - } - - return true; - } catch (Exception exception) { - return false; - } - } - } - - static { - // Override jmDNS's default topology discovery class with ours - NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() { - @Override - public NetworkTopologyDiscovery newNetworkTopologyDiscovery() { - return new MyNetworkTopologyDiscovery(); - } - }); - } - - private static JmmDNS referenceResolver() { - synchronized (MdnsDiscoveryAgent.class) { - JmmDNS instance = JmmDNS.Factory.getInstance(); - if (++resolverRefCount == 1) { - // This will cause the listener to be invoked for known hosts immediately. - // JmDNS only supports one listener per service, so we have to do this here - // with a static listener. - instance.addServiceListener(SERVICE_TYPE, nvstreamListener); - } - return instance; - } - } - - private static void dereferenceResolver() { - synchronized (MdnsDiscoveryAgent.class) { - if (--resolverRefCount == 0) { - try { - JmmDNS.Factory.close(); - } catch (IOException e) {} - } - } - } + protected HashSet computers = new HashSet<>(); public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) { this.listener = listener; } - private void handleResolvedServiceInfo(ServiceInfo info) { - synchronized (pendingResolution) { - pendingResolution.remove(info.getName()); + public abstract void startDiscovery(final int discoveryIntervalMs); + + public abstract void stopDiscovery(); + + protected void reportNewComputer(String name, int port, Inet4Address[] v4Addrs, Inet6Address[] v6Addrs) { + LimeLog.info("mDNS: "+name+" has "+v4Addrs.length+" IPv4 addresses"); + LimeLog.info("mDNS: "+name+" has "+v6Addrs.length+" IPv6 addresses"); + + Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs); + + // Add a computer object for each IPv4 address reported by the PC + for (Inet4Address v4Addr : v4Addrs) { + synchronized (computers) { + MdnsComputer computer = new MdnsComputer(name, v4Addr, v6GlobalAddr, port); + if (computers.add(computer)) { + // This was a new entry + listener.notifyComputerAdded(computer); + } + } } - try { - handleServiceInfo(info); - } catch (UnsupportedEncodingException e) { - // Invalid DNS response - LimeLog.info("mDNS: Invalid response for machine: "+info.getName()); - return; + // If there were no IPv4 addresses, use IPv6 for registration + if (v4Addrs.length == 0) { + Inet6Address v6LocalAddr = getLocalAddress(v6Addrs); + + if (v6LocalAddr != null || v6GlobalAddr != null) { + MdnsComputer computer = new MdnsComputer(name, v6LocalAddr, v6GlobalAddr, port); + if (computers.add(computer)) { + // This was a new entry + listener.notifyComputerAdded(computer); + } + } } } - private Inet6Address getLocalAddress(Inet6Address[] addresses) { + public List getComputerSet() { + synchronized (computers) { + return new ArrayList<>(computers); + } + } + + protected static Inet6Address getLocalAddress(Inet6Address[] addresses) { for (Inet6Address addr : addresses) { if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) { return addr; @@ -173,7 +72,7 @@ public class MdnsDiscoveryAgent implements ServiceListener { return null; } - private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) { + protected static Inet6Address getLinkLocalAddress(Inet6Address[] addresses) { for (Inet6Address addr : addresses) { if (addr.isLinkLocalAddress()) { LimeLog.info("Found link-local address: "+addr.getHostAddress()); @@ -184,7 +83,7 @@ public class MdnsDiscoveryAgent implements ServiceListener { return null; } - private Inet6Address getBestIpv6Address(Inet6Address[] addresses) { + protected static Inet6Address getBestIpv6Address(Inet6Address[] addresses) { // First try to find a link local address, so we can match the interface identifier // with a global address (this will work for SLAAC but not DHCPv6). Inet6Address linkLocalAddr = getLinkLocalAddress(addresses); @@ -246,139 +145,4 @@ public class MdnsDiscoveryAgent implements ServiceListener { return null; } - - private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException { - Inet4Address v4Addrs[] = info.getInet4Addresses(); - Inet6Address v6Addrs[] = info.getInet6Addresses(); - - LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses"); - LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses"); - - Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs); - - // Add a computer object for each IPv4 address reported by the PC - for (Inet4Address v4Addr : v4Addrs) { - synchronized (computers) { - MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr, info.getPort()); - if (computers.add(computer)) { - // This was a new entry - listener.notifyComputerAdded(computer); - } - } - } - - // If there were no IPv4 addresses, use IPv6 for registration - if (v4Addrs.length == 0) { - Inet6Address v6LocalAddr = getLocalAddress(v6Addrs); - - if (v6LocalAddr != null || v6GlobalAddr != null) { - MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr, info.getPort()); - if (computers.add(computer)) { - // This was a new entry - listener.notifyComputerAdded(computer); - } - } - } - } - - public void startDiscovery(final int discoveryIntervalMs) { - // Kill any existing discovery before starting a new one - stopDiscovery(); - - // Add our listener to the set - synchronized (listeners) { - listeners.add(MdnsDiscoveryAgent.this); - } - - discoveryThread = new Thread() { - @Override - public void run() { - // This may result in listener callbacks so we must register - // our listener first. - JmmDNS resolver = referenceResolver(); - - try { - while (!Thread.interrupted()) { - // Start an mDNS request - resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs); - - // Run service resolution again for pending machines - ArrayList pendingNames; - synchronized (pendingResolution) { - pendingNames = new ArrayList(pendingResolution); - } - for (String name : pendingNames) { - LimeLog.info("mDNS: Retrying service resolution for machine: "+name); - ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500); - if (infos != null && infos.length != 0) { - LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries"); - for (ServiceInfo svcinfo : infos) { - handleResolvedServiceInfo(svcinfo); - } - } - } - - // Wait for the next polling interval - try { - Thread.sleep(discoveryIntervalMs); - } catch (InterruptedException e) { - break; - } - } - } - finally { - // Dereference the resolver - dereferenceResolver(); - } - } - }; - discoveryThread.setName("mDNS Discovery Thread"); - discoveryThread.start(); - } - - public void stopDiscovery() { - // Remove our listener from the set - synchronized (listeners) { - listeners.remove(MdnsDiscoveryAgent.this); - } - - // If there's already a running thread, interrupt it - if (discoveryThread != null) { - discoveryThread.interrupt(); - discoveryThread = null; - } - } - - public List getComputerSet() { - synchronized (computers) { - return new ArrayList<>(computers); - } - } - - @Override - public void serviceAdded(ServiceEvent event) { - LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName()); - - ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500); - if (info == null) { - // This machine is pending resolution - synchronized (pendingResolution) { - pendingResolution.add(event.getInfo().getName()); - } - return; - } - - LimeLog.info("mDNS: Resolved (blocking)"); - handleResolvedServiceInfo(info); - } - - @Override - public void serviceRemoved(ServiceEvent event) { - LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName()); - } - - @Override - public void serviceResolved(ServiceEvent event) { - // We handle this synchronously - } } diff --git a/app/src/main/java/com/limelight/nvstream/mdns/NsdManagerDiscoveryAgent.java b/app/src/main/java/com/limelight/nvstream/mdns/NsdManagerDiscoveryAgent.java new file mode 100644 index 00000000..f31060b2 --- /dev/null +++ b/app/src/main/java/com/limelight/nvstream/mdns/NsdManagerDiscoveryAgent.java @@ -0,0 +1,175 @@ +package com.limelight.nvstream.mdns; + +import android.annotation.TargetApi; +import android.content.Context; +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.os.Build; + +import com.limelight.LimeLog; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +public class NsdManagerDiscoveryAgent extends MdnsDiscoveryAgent { + private static final String SERVICE_TYPE = "_nvstream._tcp"; + private NsdManager nsdManager; + private boolean discoveryActive; + private boolean wantsDiscoveryActive; + private final HashMap serviceCallbacks = new HashMap<>(); + private final ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + + private NsdManager.DiscoveryListener discoveryListener = new NsdManager.DiscoveryListener() { + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + discoveryActive = false; + LimeLog.severe("NSD: Service discovery start failed: " + errorCode); + listener.notifyDiscoveryFailure(new RuntimeException("onStartDiscoveryFailed(): " + errorCode)); + } + + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + LimeLog.severe("NSD: Service discovery stop failed: " + errorCode); + } + + @Override + public void onDiscoveryStarted(String serviceType) { + discoveryActive = true; + LimeLog.info("NSD: Service discovery started"); + + // If we were stopped before we could finish starting, stop now + if (!wantsDiscoveryActive) { + stopDiscovery(); + } + } + + @Override + public void onDiscoveryStopped(String serviceType) { + discoveryActive = false; + LimeLog.info("NSD: Service discovery stopped"); + + // If we were started before we could finish stopping, start again now + if (wantsDiscoveryActive) { + startDiscovery(0); + } + } + + @Override + public void onServiceFound(NsdServiceInfo nsdServiceInfo) { + LimeLog.info("NSD: Machine appeared: "+nsdServiceInfo.getServiceName()); + + NsdManager.ServiceInfoCallback serviceInfoCallback = new NsdManager.ServiceInfoCallback() { + @Override + public void onServiceInfoCallbackRegistrationFailed(int errorCode) { + LimeLog.severe("NSD: Service info callback registration failed: " + errorCode); + listener.notifyDiscoveryFailure(new RuntimeException("onServiceInfoCallbackRegistrationFailed(): " + errorCode)); + } + + @Override + public void onServiceUpdated(NsdServiceInfo nsdServiceInfo) { + LimeLog.info("NSD: Machine resolved: "+nsdServiceInfo.getServiceName()); + reportNewComputer(nsdServiceInfo.getServiceName(), nsdServiceInfo.getPort(), + getV4Addrs(nsdServiceInfo.getHostAddresses()), + getV6Addrs(nsdServiceInfo.getHostAddresses())); + } + + @Override + public void onServiceLost() {} + + @Override + public void onServiceInfoCallbackUnregistered() {} + }; + + nsdManager.registerServiceInfoCallback(nsdServiceInfo, executor, serviceInfoCallback); + serviceCallbacks.put(nsdServiceInfo.getServiceName(), serviceInfoCallback); + } + + @Override + public void onServiceLost(NsdServiceInfo nsdServiceInfo) { + LimeLog.info("NSD: Machine lost: "+nsdServiceInfo.getServiceName()); + + NsdManager.ServiceInfoCallback serviceInfoCallback = serviceCallbacks.remove(nsdServiceInfo.getServiceName()); + if (serviceInfoCallback != null) { + nsdManager.unregisterServiceInfoCallback(serviceInfoCallback); + } + } + }; + + public NsdManagerDiscoveryAgent(Context context, MdnsDiscoveryListener listener) { + super(listener); + this.nsdManager = context.getSystemService(NsdManager.class); + } + + @Override + public void startDiscovery(int discoveryIntervalMs) { + wantsDiscoveryActive = true; + + // Register the service discovery listener + if (!discoveryActive) { + nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener); + } + } + + @Override + public void stopDiscovery() { + wantsDiscoveryActive = false; + + // Unregister the service discovery listener + if (discoveryActive) { + nsdManager.stopServiceDiscovery(discoveryListener); + } + + // Unregister all service info callbacks + for (NsdManager.ServiceInfoCallback callback : serviceCallbacks.values()) { + nsdManager.unregisterServiceInfoCallback(callback); + } + serviceCallbacks.clear(); + } + + private static Inet4Address[] getV4Addrs(List addrs) { + int matchCount = 0; + for (InetAddress addr : addrs) { + if (addr instanceof Inet4Address) { + matchCount++; + } + } + + Inet4Address[] matching = new Inet4Address[matchCount]; + + int i = 0; + for (InetAddress addr : addrs) { + if (addr instanceof Inet4Address) { + matching[i++] = (Inet4Address) addr; + } + } + + return matching; + } + + private static Inet6Address[] getV6Addrs(List addrs) { + int matchCount = 0; + for (InetAddress addr : addrs) { + if (addr instanceof Inet6Address) { + matchCount++; + } + } + + Inet6Address[] matching = new Inet6Address[matchCount]; + + int i = 0; + for (InetAddress addr : addrs) { + if (addr instanceof Inet6Address) { + matching[i++] = (Inet6Address) addr; + } + } + + return matching; + } +}