package com.limelight.computers; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.limelight.LimeLog; import com.limelight.binding.PlatformBinding; import com.limelight.discovery.DiscoveryService; import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Binder; import android.os.IBinder; public class ComputerManagerService extends Service { private static final int MAX_CONCURRENT_REQUESTS = 4; private static final int POLLING_PERIOD_MS = 5000; private static final int MDNS_QUERY_PERIOD_MS = 1000; private ComputerManagerBinder binder = new ComputerManagerBinder(); private ComputerDatabaseManager dbManager; private AtomicInteger dbRefCount = new AtomicInteger(0); private IdentityManager idManager; private ThreadPoolExecutor pollingPool; private Timer pollingTimer; private ComputerManagerListener listener = null; private DiscoveryService.DiscoveryBinder discoveryBinder; private ServiceConnection discoveryServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder binder) { synchronized (discoveryServiceConnection) { DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder); // Set us as the event listener privateBinder.setListener(createDiscoveryListener()); // Signal a possible waiter that we're all setup discoveryBinder = privateBinder; discoveryServiceConnection.notifyAll(); } } public void onServiceDisconnected(ComponentName className) { discoveryBinder = null; } }; public class ComputerManagerBinder extends Binder { public void startPolling(ComputerManagerListener listener) { // Set the listener ComputerManagerService.this.listener = listener; // Start mDNS autodiscovery too discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS); // Start polling known machines pollingTimer = new Timer(); pollingTimer.schedule(getTimerTask(), 0, POLLING_PERIOD_MS); } public void waitForReady() { synchronized (discoveryServiceConnection) { try { while (discoveryBinder == null) { // Wait for the bind notification discoveryServiceConnection.wait(1000); } } catch (InterruptedException e) { } } } public boolean addComputerBlocking(InetAddress addr) { return ComputerManagerService.this.addComputerBlocking(addr); } public void addComputer(InetAddress addr) { ComputerManagerService.this.addComputer(addr); } public void removeComputer(String name) { ComputerManagerService.this.removeComputer(name); } public void stopPolling() { // Just call the unbind handler to cleanup ComputerManagerService.this.onUnbind(null); } public String getUniqueId() { return idManager.getUniqueId(); } } @Override public boolean onUnbind(Intent intent) { // Stop mDNS autodiscovery discoveryBinder.stopDiscovery(); // Stop polling if (pollingTimer != null) { pollingTimer.cancel(); pollingTimer = null; } // Remove the listener listener = null; return false; } private MdnsDiscoveryListener createDiscoveryListener() { return new MdnsDiscoveryListener() { @Override public void notifyComputerAdded(MdnsComputer computer) { // Kick off a serverinfo poll on this machine addComputer(computer.getAddress()); } @Override public void notifyComputerRemoved(MdnsComputer computer) { // Nothing to do here } @Override public void notifyDiscoveryFailure(Exception e) { LimeLog.severe("mDNS discovery failed"); e.printStackTrace(); } }; } public void addComputer(InetAddress addr) { // Setup a placeholder ComputerDetails fakeDetails = new ComputerDetails(); fakeDetails.localIp = addr; fakeDetails.remoteIp = addr; // Put it in the thread pool to process later pollingPool.execute(getPollingRunnable(fakeDetails)); } public boolean addComputerBlocking(InetAddress addr) { // Setup a placeholder ComputerDetails fakeDetails = new ComputerDetails(); fakeDetails.localIp = addr; fakeDetails.remoteIp = addr; // Block while we try to fill the details getPollingRunnable(fakeDetails).run(); // If the machine is reachable, it was successful return fakeDetails.state == ComputerDetails.State.ONLINE; } public void removeComputer(String name) { if (!getLocalDatabaseReference()) { return; } // Remove it from the database dbManager.deleteComputer(name); releaseLocalDatabaseReference(); } private boolean getLocalDatabaseReference() { if (dbRefCount.get() == 0) { return false; } dbRefCount.incrementAndGet(); return true; } private void releaseLocalDatabaseReference() { if (dbRefCount.decrementAndGet() == 0) { dbManager.close(); } } private TimerTask getTimerTask() { return new TimerTask() { @Override public void run() { if (!getLocalDatabaseReference()) { return; } List computerList = dbManager.getAllComputers(); releaseLocalDatabaseReference(); for (ComputerDetails computer : computerList) { pollingPool.execute(getPollingRunnable(computer)); } } }; } private int getActiveNetworkType() { ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo(); if (activeNetworkInfo == null) { return -1; } return activeNetworkInfo.getType(); } private InterfaceAddress getActiveInterfaceAddress() { String matchingPrefix; switch (getActiveNetworkType()) { case ConnectivityManager.TYPE_ETHERNET: matchingPrefix = "eth"; break; case ConnectivityManager.TYPE_WIFI: matchingPrefix = "wlan"; break; default: // Must be on Ethernet or Wifi to consider that we can send large packets return null; } // Try to find the interface that corresponds to the active network try { Enumeration ifaceList = NetworkInterface.getNetworkInterfaces(); while (ifaceList.hasMoreElements()) { NetworkInterface iface = ifaceList.nextElement(); // Look for an interface that matches the prefix we expect if (iface.isUp() && iface.getName().startsWith(matchingPrefix)) { // Find the IPv4 address for the interface for (InterfaceAddress addr : iface.getInterfaceAddresses()) { if (!(addr.getAddress() instanceof Inet4Address)) { // Skip non-IPv4 addresses continue; } // Found the right address on the right interface return addr; } } } } catch (SocketException e) { e.printStackTrace(); } // We didn't find the interface or something else went wrong return null; } private boolean isOnSameSubnet(InetAddress targetAddress, InetAddress localAddress, short networkPrefixLength) { byte[] targetBytes = targetAddress.getAddress(); byte[] localBytes = localAddress.getAddress(); for (int byteIndex = 0; networkPrefixLength > 0; byteIndex++) { byte target = targetBytes[byteIndex]; byte local = localBytes[byteIndex]; if (networkPrefixLength >= 8) { // Do a full byte comparison if (target != local) { return false; } networkPrefixLength -= 8; } else { target &= (byte)(0xFF << (8 - networkPrefixLength)); local &= (byte)(0xFF << (8 - networkPrefixLength)); // Do a masked comparison if (target != local) { return false; } networkPrefixLength = 0; } } return true; } private ComputerDetails tryPollIp(InetAddress ipAddr) { try { NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(), null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); return http.getComputerDetails(); } catch (Exception e) { return null; } } private boolean pollComputer(ComputerDetails details, boolean localFirst) { ComputerDetails polledDetails; if (localFirst) { polledDetails = tryPollIp(details.localIp); } else { polledDetails = tryPollIp(details.remoteIp); } if (polledDetails == null) { // Failed, so let's try the fallback if (!localFirst) { polledDetails = tryPollIp(details.localIp); } else { polledDetails = tryPollIp(details.remoteIp); } // The fallback poll worked if (polledDetails != null) { polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE; } } else { polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE; } // Machine was unreachable both tries if (polledDetails == null) { return false; } // If we got here, it's reachable details.update(polledDetails); return true; } private boolean doPollMachine(ComputerDetails details) { // Get the network type int networkType = getActiveNetworkType(); switch (networkType) { // We'll check local first on these if we find // we're on the same subnet case ConnectivityManager.TYPE_ETHERNET: case ConnectivityManager.TYPE_WIFI: InterfaceAddress ifaceAddr = getActiveInterfaceAddress(); if (ifaceAddr != null) { if (isOnSameSubnet(details.localIp, ifaceAddr.getAddress(), ifaceAddr.getNetworkPrefixLength())) { // It's on the same subnet, so poll local first LimeLog.info("Machine looks local; trying local IP first"); return pollComputer(details, true); } } // Fall through to remote first default: LimeLog.info("Machine looks remote; trying remote IP first"); return pollComputer(details, false); } } private Runnable getPollingRunnable(final ComputerDetails details) { return new Runnable() { @Override public void run() { boolean newPc = (details.name == null); if (!getLocalDatabaseReference()) { return; } // Poll the machine if (!doPollMachine(details)) { details.state = ComputerDetails.State.OFFLINE; details.reachability = ComputerDetails.Reachability.OFFLINE; } // If it's online, update our persistent state if (details.state == ComputerDetails.State.ONLINE) { if (!newPc) { // Check if it's in the database because it could have been // removed after this was issued if (dbManager.getComputerByName(details.name) == null) { // It's gone releaseLocalDatabaseReference(); return; } } dbManager.updateComputer(details); } // Don't call the listener if this is a failed lookup of a new PC if ((!newPc || details.state == ComputerDetails.State.ONLINE) && listener != null) { listener.notifyComputerUpdated(details); } releaseLocalDatabaseReference(); } }; } @Override public void onCreate() { // Bind to the discovery service bindService(new Intent(this, DiscoveryService.class), discoveryServiceConnection, Service.BIND_AUTO_CREATE); // Create the thread pool for updating computer state pollingPool = new ThreadPoolExecutor(MAX_CONCURRENT_REQUESTS, MAX_CONCURRENT_REQUESTS, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue(), new ThreadPoolExecutor.DiscardPolicy()); // Lookup or generate this device's UID idManager = new IdentityManager(this); // Initialize the DB dbManager = new ComputerDatabaseManager(this); dbRefCount.set(1); } @Override public void onDestroy() { if (discoveryBinder != null) { // Unbind from the discovery service unbindService(discoveryServiceConnection); } // Stop the thread pool pollingPool.shutdownNow(); // FIXME: Should await termination here but we have timeout issues in HttpURLConnection // Remove the initial DB reference releaseLocalDatabaseReference(); } @Override public IBinder onBind(Intent intent) { return binder; } }