Loading core/java/android/net/nsd/NsdManager.java +81 −103 Original line number Diff line number Diff line Loading @@ -16,6 +16,10 @@ package android.net.nsd; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkStringNotEmpty; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; Loading Loading @@ -241,12 +245,12 @@ public final class NsdManager { return name; } private static int FIRST_LISTENER_KEY = 1; private final INsdManager mService; private final Context mContext; private static final int INVALID_LISTENER_KEY = 0; private static final int BUSY_LISTENER_KEY = -1; private int mListenerKey = 1; private int mListenerKey = FIRST_LISTENER_KEY; private final SparseArray mListenerMap = new SparseArray(); private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>(); private final Object mMapLock = new Object(); Loading @@ -269,6 +273,14 @@ public final class NsdManager { init(); } /** * @hide */ @VisibleForTesting public void disconnect() { mAsyncChannel.disconnect(); } /** * Failures are passed with {@link RegistrationListener#onRegistrationFailed}, * {@link RegistrationListener#onUnregistrationFailed}, Loading Loading @@ -304,7 +316,6 @@ public final class NsdManager { public void onServiceFound(NsdServiceInfo serviceInfo); public void onServiceLost(NsdServiceInfo serviceInfo); } /** Interface for callback invocation for service registration */ Loading Loading @@ -335,8 +346,9 @@ public final class NsdManager { @Override public void handleMessage(Message message) { if (DBG) Log.d(TAG, "received " + nameOf(message.what)); switch (message.what) { final int what = message.what; final int key = message.arg2; switch (what) { case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: mAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION); return; Loading @@ -349,19 +361,26 @@ public final class NsdManager { default: break; } Object listener = getListener(message.arg2); final Object listener; final NsdServiceInfo ns; synchronized (mMapLock) { listener = mListenerMap.get(key); ns = mServiceMap.get(key); } if (listener == null) { Log.d(TAG, "Stale key " + message.arg2); return; } NsdServiceInfo ns = getNsdService(message.arg2); switch (message.what) { if (DBG) { Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns); } switch (what) { case DISCOVER_SERVICES_STARTED: String s = getNsdServiceInfoType((NsdServiceInfo) message.obj); ((DiscoveryListener) listener).onDiscoveryStarted(s); break; case DISCOVER_SERVICES_FAILED: removeListener(message.arg2); removeListener(key); ((DiscoveryListener) listener).onStartDiscoveryFailed(getNsdServiceInfoType(ns), message.arg1); break; Loading @@ -374,16 +393,16 @@ public final class NsdManager { case STOP_DISCOVERY_FAILED: // TODO: failure to stop discovery should be internal and retried internally, as // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED removeListener(message.arg2); removeListener(key); ((DiscoveryListener) listener).onStopDiscoveryFailed(getNsdServiceInfoType(ns), message.arg1); break; case STOP_DISCOVERY_SUCCEEDED: removeListener(message.arg2); removeListener(key); ((DiscoveryListener) listener).onDiscoveryStopped(getNsdServiceInfoType(ns)); break; case REGISTER_SERVICE_FAILED: removeListener(message.arg2); removeListener(key); ((RegistrationListener) listener).onRegistrationFailed(ns, message.arg1); break; case REGISTER_SERVICE_SUCCEEDED: Loading @@ -391,7 +410,7 @@ public final class NsdManager { (NsdServiceInfo) message.obj); break; case UNREGISTER_SERVICE_FAILED: removeListener(message.arg2); removeListener(key); ((RegistrationListener) listener).onUnregistrationFailed(ns, message.arg1); break; case UNREGISTER_SERVICE_SUCCEEDED: Loading @@ -401,11 +420,11 @@ public final class NsdManager { ((RegistrationListener) listener).onServiceUnregistered(ns); break; case RESOLVE_SERVICE_FAILED: removeListener(message.arg2); removeListener(key); ((ResolveListener) listener).onResolveFailed(ns, message.arg1); break; case RESOLVE_SERVICE_SUCCEEDED: removeListener(message.arg2); removeListener(key); ((ResolveListener) listener).onServiceResolved((NsdServiceInfo) message.obj); break; default: Loading @@ -415,40 +434,27 @@ public final class NsdManager { } } // if the listener is already in the map, reject it. Otherwise, add it and // return its key. private int nextListenerKey() { // Ensure mListenerKey >= FIRST_LISTENER_KEY; mListenerKey = Math.max(FIRST_LISTENER_KEY, mListenerKey + 1); return mListenerKey; } // Assert that the listener is not in the map, then add it and returns its key private int putListener(Object listener, NsdServiceInfo s) { if (listener == null) return INVALID_LISTENER_KEY; int key; checkListener(listener); final int key; synchronized (mMapLock) { int valueIndex = mListenerMap.indexOfValue(listener); if (valueIndex != -1) { return BUSY_LISTENER_KEY; } do { key = mListenerKey++; } while (key == INVALID_LISTENER_KEY); checkArgument(valueIndex == -1, "listener already in use"); key = nextListenerKey(); mListenerMap.put(key, listener); mServiceMap.put(key, s); } return key; } private Object getListener(int key) { if (key == INVALID_LISTENER_KEY) return null; synchronized (mMapLock) { return mListenerMap.get(key); } } private NsdServiceInfo getNsdService(int key) { synchronized (mMapLock) { return mServiceMap.get(key); } } private void removeListener(int key) { if (key == INVALID_LISTENER_KEY) return; synchronized (mMapLock) { mListenerMap.remove(key); mServiceMap.remove(key); Loading @@ -456,16 +462,15 @@ public final class NsdManager { } private int getListenerKey(Object listener) { checkListener(listener); synchronized (mMapLock) { int valueIndex = mListenerMap.indexOfValue(listener); if (valueIndex != -1) { checkArgument(valueIndex != -1, "listener not registered"); return mListenerMap.keyAt(valueIndex); } } return INVALID_LISTENER_KEY; } private String getNsdServiceInfoType(NsdServiceInfo s) { private static String getNsdServiceInfoType(NsdServiceInfo s) { if (s == null) return "?"; return s.getServiceType(); } Loading @@ -475,7 +480,9 @@ public final class NsdManager { */ private void init() { final Messenger messenger = getMessenger(); if (messenger == null) throw new RuntimeException("Failed to initialize"); if (messenger == null) { fatal("Failed to obtain service Messenger"); } HandlerThread t = new HandlerThread("NsdManager"); t.start(); mHandler = new ServiceHandler(t.getLooper()); Loading @@ -483,8 +490,13 @@ public final class NsdManager { try { mConnected.await(); } catch (InterruptedException e) { Log.e(TAG, "interrupted wait at init"); fatal("Interrupted wait at init"); } } private static void fatal(String msg) { Log.e(TAG, msg); throw new RuntimeException(msg); } /** Loading @@ -506,23 +518,10 @@ public final class NsdManager { */ public void registerService(NsdServiceInfo serviceInfo, int protocolType, RegistrationListener listener) { if (TextUtils.isEmpty(serviceInfo.getServiceName()) || TextUtils.isEmpty(serviceInfo.getServiceType())) { throw new IllegalArgumentException("Service name or type cannot be empty"); } if (serviceInfo.getPort() <= 0) { throw new IllegalArgumentException("Invalid port number"); } if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } if (protocolType != PROTOCOL_DNS_SD) { throw new IllegalArgumentException("Unsupported protocol"); } checkArgument(serviceInfo.getPort() > 0, "Invalid port number"); checkServiceInfo(serviceInfo); checkProtocol(protocolType); int key = putListener(listener, serviceInfo); if (key == BUSY_LISTENER_KEY) { throw new IllegalArgumentException("listener already in use"); } mAsyncChannel.sendMessage(REGISTER_SERVICE, 0, key, serviceInfo); } Loading @@ -541,12 +540,6 @@ public final class NsdManager { */ public void unregisterService(RegistrationListener listener) { int id = getListenerKey(listener); if (id == INVALID_LISTENER_KEY) { throw new IllegalArgumentException("listener not registered"); } if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } mAsyncChannel.sendMessage(UNREGISTER_SERVICE, 0, id); } Loading Loading @@ -579,25 +572,13 @@ public final class NsdManager { * Cannot be null. Cannot be in use for an active service discovery. */ public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } if (TextUtils.isEmpty(serviceType)) { throw new IllegalArgumentException("Service type cannot be empty"); } if (protocolType != PROTOCOL_DNS_SD) { throw new IllegalArgumentException("Unsupported protocol"); } checkStringNotEmpty(serviceType, "Service type cannot be empty"); checkProtocol(protocolType); NsdServiceInfo s = new NsdServiceInfo(); s.setServiceType(serviceType); int key = putListener(listener, s); if (key == BUSY_LISTENER_KEY) { throw new IllegalArgumentException("listener already in use"); } mAsyncChannel.sendMessage(DISCOVER_SERVICES, 0, key, s); } Loading @@ -619,12 +600,6 @@ public final class NsdManager { */ public void stopServiceDiscovery(DiscoveryListener listener) { int id = getListenerKey(listener); if (id == INVALID_LISTENER_KEY) { throw new IllegalArgumentException("service discovery not active on listener"); } if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } mAsyncChannel.sendMessage(STOP_DISCOVERY, 0, id); } Loading @@ -638,19 +613,8 @@ public final class NsdManager { * Cannot be in use for an active service resolution. */ public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) { if (TextUtils.isEmpty(serviceInfo.getServiceName()) || TextUtils.isEmpty(serviceInfo.getServiceType())) { throw new IllegalArgumentException("Service name or type cannot be empty"); } if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } checkServiceInfo(serviceInfo); int key = putListener(listener, serviceInfo); if (key == BUSY_LISTENER_KEY) { throw new IllegalArgumentException("listener already in use"); } mAsyncChannel.sendMessage(RESOLVE_SERVICE, 0, key, serviceInfo); } Loading @@ -664,10 +628,10 @@ public final class NsdManager { } /** * Get a reference to NetworkService handler. This is used to establish * Get a reference to NsdService handler. This is used to establish * an AsyncChannel communication with the service * * @return Messenger pointing to the NetworkService handler * @return Messenger pointing to the NsdService handler */ private Messenger getMessenger() { try { Loading @@ -676,4 +640,18 @@ public final class NsdManager { throw e.rethrowFromSystemServer(); } } private static void checkListener(Object listener) { checkNotNull(listener, "listener cannot be null"); } private static void checkProtocol(int protocolType) { checkArgument(protocolType == PROTOCOL_DNS_SD, "Unsupported protocol"); } private static void checkServiceInfo(NsdServiceInfo serviceInfo) { checkNotNull(serviceInfo, "NsdServiceInfo cannot be null"); checkStringNotEmpty(serviceInfo.getServiceName(),"Service name cannot be empty"); checkStringNotEmpty(serviceInfo.getServiceType(), "Service type cannot be empty"); } } services/core/java/com/android/server/NsdService.java +22 −39 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import android.provider.Settings; import android.util.Base64; import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import java.io.FileDescriptor; import java.io.PrintWriter; Loading @@ -48,7 +49,6 @@ import com.android.internal.util.AsyncChannel; import com.android.internal.util.Protocol; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.server.NativeDaemonConnector.Command; /** * Network Service Discovery Service handles remote service discovery operation requests by Loading Loading @@ -161,7 +161,7 @@ public class NsdService extends INsdManager.Stub { } //Last client if (mClients.size() == 0) { stopMDnsDaemon(); mDaemon.stop(); } break; case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: Loading Loading @@ -221,14 +221,14 @@ public class NsdService extends INsdManager.Stub { public void enter() { sendNsdStateChangeBroadcast(true); if (mClients.size() > 0) { startMDnsDaemon(); mDaemon.start(); } } @Override public void exit() { if (mClients.size() > 0) { stopMDnsDaemon(); mDaemon.stop(); } } Loading @@ -247,8 +247,8 @@ public class NsdService extends INsdManager.Stub { } private void removeRequestMap(int clientId, int globalId, ClientInfo clientInfo) { clientInfo.mClientIds.remove(clientId); clientInfo.mClientRequests.remove(clientId); clientInfo.mClientIds.delete(clientId); clientInfo.mClientRequests.delete(clientId); mIdToClientInfoMap.remove(globalId); } Loading @@ -262,7 +262,7 @@ public class NsdService extends INsdManager.Stub { //First client if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL && mClients.size() == 0) { startMDnsDaemon(); mDaemon.start(); } return NOT_HANDLED; case AsyncChannel.CMD_CHANNEL_DISCONNECTED: Loading Loading @@ -301,7 +301,7 @@ public class NsdService extends INsdManager.Stub { clientInfo = mClients.get(msg.replyTo); try { id = clientInfo.mClientIds.get(msg.arg2).intValue(); id = clientInfo.mClientIds.get(msg.arg2); } catch (NullPointerException e) { replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED, NsdManager.FAILURE_INTERNAL_ERROR); Loading Loading @@ -339,7 +339,7 @@ public class NsdService extends INsdManager.Stub { if (DBG) Slog.d(TAG, "unregister service"); clientInfo = mClients.get(msg.replyTo); try { id = clientInfo.mClientIds.get(msg.arg2).intValue(); id = clientInfo.mClientIds.get(msg.arg2); } catch (NullPointerException e) { replyToMessage(msg, NsdManager.UNREGISTER_SERVICE_FAILED, NsdManager.FAILURE_INTERNAL_ERROR); Loading Loading @@ -712,26 +712,13 @@ public class NsdService extends INsdManager.Stub { return true; } public boolean execute(Command cmd) { if (DBG) { Slog.d(TAG, cmd.toString()); } try { mNativeConnector.execute(cmd); } catch (NativeDaemonConnectorException e) { Slog.e(TAG, "Failed to execute " + cmd, e); return false; } return true; } public void start() { execute("start-service"); } private boolean startMDnsDaemon() { return mDaemon.execute("start-service"); public void stop() { execute("stop-service"); } private boolean stopMDnsDaemon() { return mDaemon.execute("stop-service"); } private boolean registerService(int regId, NsdServiceInfo service) { Loading @@ -743,8 +730,7 @@ public class NsdService extends INsdManager.Stub { int port = service.getPort(); byte[] textRecord = service.getTxtRecord(); String record = Base64.encodeToString(textRecord, Base64.DEFAULT).replace("\n", ""); Command cmd = new Command("mdnssd", "register", regId, name, type, port, record); return mDaemon.execute(cmd); return mDaemon.execute("register", regId, name, type, port, record); } private boolean unregisterService(int regId) { Loading Loading @@ -843,10 +829,10 @@ public class NsdService extends INsdManager.Stub { private NsdServiceInfo mResolvedService; /* A map from client id to unique id sent to mDns */ private final SparseArray<Integer> mClientIds = new SparseArray<>(); private final SparseIntArray mClientIds = new SparseIntArray(); /* A map from client id to the type of the request we had received */ private final SparseArray<Integer> mClientRequests = new SparseArray<>(); private final SparseIntArray mClientRequests = new SparseIntArray(); private ClientInfo(AsyncChannel c, Messenger m) { mChannel = c; Loading @@ -873,6 +859,7 @@ public class NsdService extends INsdManager.Stub { // and send cancellations to the daemon. private void expungeAllRequests() { int globalId, clientId, i; // TODO: to keep handler responsive, do not clean all requests for that client at once. for (i = 0; i < mClientIds.size(); i++) { clientId = mClientIds.keyAt(i); globalId = mClientIds.valueAt(i); Loading Loading @@ -900,15 +887,11 @@ public class NsdService extends INsdManager.Stub { // mClientIds is a sparse array of listener id -> mDnsClient id. For a given mDnsClient id, // return the corresponding listener id. mDnsClient id is also called a global id. private int getClientId(final int globalId) { // This doesn't use mClientIds.indexOfValue because indexOfValue uses == (not .equals) // while also coercing the int primitives to Integer objects. for (int i = 0, nSize = mClientIds.size(); i < nSize; i++) { int mDnsId = mClientIds.valueAt(i); if (globalId == mDnsId) { return mClientIds.keyAt(i); } int idx = mClientIds.indexOfValue(globalId); if (idx < 0) { return idx; } return -1; return mClientIds.keyAt(idx); } } Loading tests/net/java/android/net/nsd/NsdServiceTest.java +84 −13 Original line number Diff line number Diff line Loading @@ -16,68 +16,121 @@ package com.android.server; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.test.TestLooper; import android.content.Context; import android.content.ContentResolver; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import com.android.server.NsdService.DaemonConnection; import com.android.server.NsdService.DaemonConnectionSupplier; import com.android.server.NsdService.NativeCallbackReceiver; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; // TODOs: // - test client disconnects // - test client can send requests and receive replies // - test NSD_ON ENABLE/DISABLED listening @RunWith(AndroidJUnit4.class) @SmallTest public class NsdServiceTest { static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD; long mTimeoutMs = 100; // non-final so that tests can adjust the value. @Mock Context mContext; @Mock ContentResolver mResolver; @Mock NsdService.NsdSettings mSettings; @Mock DaemonConnection mDaemon; NativeCallbackReceiver mDaemonCallback; TestLooper mLooper; HandlerThread mThread; TestHandler mHandler; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mLooper = new TestLooper(); mHandler = new TestHandler(mLooper.getLooper()); mThread = new HandlerThread("mock-service-handler"); mThread.start(); mHandler = new TestHandler(mThread.getLooper()); when(mContext.getContentResolver()).thenReturn(mResolver); } @After public void tearDown() throws Exception { mThread.quit(); } @Test public void testClientsCanConnect() { public void testClientsCanConnectAndDisconnect() { when(mSettings.isEnabled()).thenReturn(true); NsdService service = makeService(); NsdManager client1 = connectClient(service); verify(mDaemon, timeout(100).times(1)).execute("start-service"); verify(mDaemon, timeout(100).times(1)).start(); NsdManager client2 = connectClient(service); // TODO: disconnect client1 // TODO: disconnect client2 client1.disconnect(); client2.disconnect(); verify(mDaemon, timeout(mTimeoutMs).times(1)).stop(); } @Test public void testClientRequestsAreGCedAtDisconnection() { when(mSettings.isEnabled()).thenReturn(true); when(mDaemon.execute(any())).thenReturn(true); NsdService service = makeService(); NsdManager client = connectClient(service); verify(mDaemon, timeout(100).times(1)).start(); NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); request.setPort(2201); // Client registration request NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); client.registerService(request, PROTOCOL, listener1); verifyDaemonCommand("register 2 a_name a_type 2201"); // Client discovery request NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class); client.discoverServices("a_type", PROTOCOL, listener2); verifyDaemonCommand("discover 3 a_type"); // Client resolve request NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class); client.resolveService(request, listener3); verifyDaemonCommand("resolve 4 a_name a_type local."); // Client disconnects client.disconnect(); verify(mDaemon, timeout(mTimeoutMs).times(1)).stop(); // checks that request are cleaned verifyDaemonCommands("stop-register 2", "stop-discover 3", "stop-resolve 4"); } NsdService makeService() { Loading @@ -91,10 +144,28 @@ public class NsdServiceTest { } NsdManager connectClient(NsdService service) { mLooper.startAutoDispatch(); NsdManager client = new NsdManager(mContext, service); mLooper.stopAutoDispatch(); return client; return new NsdManager(mContext, service); } void verifyDaemonCommands(String... wants) { verifyDaemonCommand(String.join(" ", wants), wants.length); } void verifyDaemonCommand(String want) { verifyDaemonCommand(want, 1); } void verifyDaemonCommand(String want, int n) { ArgumentCaptor<Object> argumentsCaptor = ArgumentCaptor.forClass(Object.class); verify(mDaemon, timeout(mTimeoutMs).times(n)).execute(argumentsCaptor.capture()); String got = ""; for (Object o : argumentsCaptor.getAllValues()) { got += o + " "; } assertEquals(want, got.trim()); // rearm deamon for next command verification reset(mDaemon); when(mDaemon.execute(any())).thenReturn(true); } public static class TestHandler extends Handler { Loading Loading
core/java/android/net/nsd/NsdManager.java +81 −103 Original line number Diff line number Diff line Loading @@ -16,6 +16,10 @@ package android.net.nsd; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkStringNotEmpty; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; Loading Loading @@ -241,12 +245,12 @@ public final class NsdManager { return name; } private static int FIRST_LISTENER_KEY = 1; private final INsdManager mService; private final Context mContext; private static final int INVALID_LISTENER_KEY = 0; private static final int BUSY_LISTENER_KEY = -1; private int mListenerKey = 1; private int mListenerKey = FIRST_LISTENER_KEY; private final SparseArray mListenerMap = new SparseArray(); private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>(); private final Object mMapLock = new Object(); Loading @@ -269,6 +273,14 @@ public final class NsdManager { init(); } /** * @hide */ @VisibleForTesting public void disconnect() { mAsyncChannel.disconnect(); } /** * Failures are passed with {@link RegistrationListener#onRegistrationFailed}, * {@link RegistrationListener#onUnregistrationFailed}, Loading Loading @@ -304,7 +316,6 @@ public final class NsdManager { public void onServiceFound(NsdServiceInfo serviceInfo); public void onServiceLost(NsdServiceInfo serviceInfo); } /** Interface for callback invocation for service registration */ Loading Loading @@ -335,8 +346,9 @@ public final class NsdManager { @Override public void handleMessage(Message message) { if (DBG) Log.d(TAG, "received " + nameOf(message.what)); switch (message.what) { final int what = message.what; final int key = message.arg2; switch (what) { case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: mAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION); return; Loading @@ -349,19 +361,26 @@ public final class NsdManager { default: break; } Object listener = getListener(message.arg2); final Object listener; final NsdServiceInfo ns; synchronized (mMapLock) { listener = mListenerMap.get(key); ns = mServiceMap.get(key); } if (listener == null) { Log.d(TAG, "Stale key " + message.arg2); return; } NsdServiceInfo ns = getNsdService(message.arg2); switch (message.what) { if (DBG) { Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns); } switch (what) { case DISCOVER_SERVICES_STARTED: String s = getNsdServiceInfoType((NsdServiceInfo) message.obj); ((DiscoveryListener) listener).onDiscoveryStarted(s); break; case DISCOVER_SERVICES_FAILED: removeListener(message.arg2); removeListener(key); ((DiscoveryListener) listener).onStartDiscoveryFailed(getNsdServiceInfoType(ns), message.arg1); break; Loading @@ -374,16 +393,16 @@ public final class NsdManager { case STOP_DISCOVERY_FAILED: // TODO: failure to stop discovery should be internal and retried internally, as // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED removeListener(message.arg2); removeListener(key); ((DiscoveryListener) listener).onStopDiscoveryFailed(getNsdServiceInfoType(ns), message.arg1); break; case STOP_DISCOVERY_SUCCEEDED: removeListener(message.arg2); removeListener(key); ((DiscoveryListener) listener).onDiscoveryStopped(getNsdServiceInfoType(ns)); break; case REGISTER_SERVICE_FAILED: removeListener(message.arg2); removeListener(key); ((RegistrationListener) listener).onRegistrationFailed(ns, message.arg1); break; case REGISTER_SERVICE_SUCCEEDED: Loading @@ -391,7 +410,7 @@ public final class NsdManager { (NsdServiceInfo) message.obj); break; case UNREGISTER_SERVICE_FAILED: removeListener(message.arg2); removeListener(key); ((RegistrationListener) listener).onUnregistrationFailed(ns, message.arg1); break; case UNREGISTER_SERVICE_SUCCEEDED: Loading @@ -401,11 +420,11 @@ public final class NsdManager { ((RegistrationListener) listener).onServiceUnregistered(ns); break; case RESOLVE_SERVICE_FAILED: removeListener(message.arg2); removeListener(key); ((ResolveListener) listener).onResolveFailed(ns, message.arg1); break; case RESOLVE_SERVICE_SUCCEEDED: removeListener(message.arg2); removeListener(key); ((ResolveListener) listener).onServiceResolved((NsdServiceInfo) message.obj); break; default: Loading @@ -415,40 +434,27 @@ public final class NsdManager { } } // if the listener is already in the map, reject it. Otherwise, add it and // return its key. private int nextListenerKey() { // Ensure mListenerKey >= FIRST_LISTENER_KEY; mListenerKey = Math.max(FIRST_LISTENER_KEY, mListenerKey + 1); return mListenerKey; } // Assert that the listener is not in the map, then add it and returns its key private int putListener(Object listener, NsdServiceInfo s) { if (listener == null) return INVALID_LISTENER_KEY; int key; checkListener(listener); final int key; synchronized (mMapLock) { int valueIndex = mListenerMap.indexOfValue(listener); if (valueIndex != -1) { return BUSY_LISTENER_KEY; } do { key = mListenerKey++; } while (key == INVALID_LISTENER_KEY); checkArgument(valueIndex == -1, "listener already in use"); key = nextListenerKey(); mListenerMap.put(key, listener); mServiceMap.put(key, s); } return key; } private Object getListener(int key) { if (key == INVALID_LISTENER_KEY) return null; synchronized (mMapLock) { return mListenerMap.get(key); } } private NsdServiceInfo getNsdService(int key) { synchronized (mMapLock) { return mServiceMap.get(key); } } private void removeListener(int key) { if (key == INVALID_LISTENER_KEY) return; synchronized (mMapLock) { mListenerMap.remove(key); mServiceMap.remove(key); Loading @@ -456,16 +462,15 @@ public final class NsdManager { } private int getListenerKey(Object listener) { checkListener(listener); synchronized (mMapLock) { int valueIndex = mListenerMap.indexOfValue(listener); if (valueIndex != -1) { checkArgument(valueIndex != -1, "listener not registered"); return mListenerMap.keyAt(valueIndex); } } return INVALID_LISTENER_KEY; } private String getNsdServiceInfoType(NsdServiceInfo s) { private static String getNsdServiceInfoType(NsdServiceInfo s) { if (s == null) return "?"; return s.getServiceType(); } Loading @@ -475,7 +480,9 @@ public final class NsdManager { */ private void init() { final Messenger messenger = getMessenger(); if (messenger == null) throw new RuntimeException("Failed to initialize"); if (messenger == null) { fatal("Failed to obtain service Messenger"); } HandlerThread t = new HandlerThread("NsdManager"); t.start(); mHandler = new ServiceHandler(t.getLooper()); Loading @@ -483,8 +490,13 @@ public final class NsdManager { try { mConnected.await(); } catch (InterruptedException e) { Log.e(TAG, "interrupted wait at init"); fatal("Interrupted wait at init"); } } private static void fatal(String msg) { Log.e(TAG, msg); throw new RuntimeException(msg); } /** Loading @@ -506,23 +518,10 @@ public final class NsdManager { */ public void registerService(NsdServiceInfo serviceInfo, int protocolType, RegistrationListener listener) { if (TextUtils.isEmpty(serviceInfo.getServiceName()) || TextUtils.isEmpty(serviceInfo.getServiceType())) { throw new IllegalArgumentException("Service name or type cannot be empty"); } if (serviceInfo.getPort() <= 0) { throw new IllegalArgumentException("Invalid port number"); } if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } if (protocolType != PROTOCOL_DNS_SD) { throw new IllegalArgumentException("Unsupported protocol"); } checkArgument(serviceInfo.getPort() > 0, "Invalid port number"); checkServiceInfo(serviceInfo); checkProtocol(protocolType); int key = putListener(listener, serviceInfo); if (key == BUSY_LISTENER_KEY) { throw new IllegalArgumentException("listener already in use"); } mAsyncChannel.sendMessage(REGISTER_SERVICE, 0, key, serviceInfo); } Loading @@ -541,12 +540,6 @@ public final class NsdManager { */ public void unregisterService(RegistrationListener listener) { int id = getListenerKey(listener); if (id == INVALID_LISTENER_KEY) { throw new IllegalArgumentException("listener not registered"); } if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } mAsyncChannel.sendMessage(UNREGISTER_SERVICE, 0, id); } Loading Loading @@ -579,25 +572,13 @@ public final class NsdManager { * Cannot be null. Cannot be in use for an active service discovery. */ public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } if (TextUtils.isEmpty(serviceType)) { throw new IllegalArgumentException("Service type cannot be empty"); } if (protocolType != PROTOCOL_DNS_SD) { throw new IllegalArgumentException("Unsupported protocol"); } checkStringNotEmpty(serviceType, "Service type cannot be empty"); checkProtocol(protocolType); NsdServiceInfo s = new NsdServiceInfo(); s.setServiceType(serviceType); int key = putListener(listener, s); if (key == BUSY_LISTENER_KEY) { throw new IllegalArgumentException("listener already in use"); } mAsyncChannel.sendMessage(DISCOVER_SERVICES, 0, key, s); } Loading @@ -619,12 +600,6 @@ public final class NsdManager { */ public void stopServiceDiscovery(DiscoveryListener listener) { int id = getListenerKey(listener); if (id == INVALID_LISTENER_KEY) { throw new IllegalArgumentException("service discovery not active on listener"); } if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } mAsyncChannel.sendMessage(STOP_DISCOVERY, 0, id); } Loading @@ -638,19 +613,8 @@ public final class NsdManager { * Cannot be in use for an active service resolution. */ public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) { if (TextUtils.isEmpty(serviceInfo.getServiceName()) || TextUtils.isEmpty(serviceInfo.getServiceType())) { throw new IllegalArgumentException("Service name or type cannot be empty"); } if (listener == null) { throw new IllegalArgumentException("listener cannot be null"); } checkServiceInfo(serviceInfo); int key = putListener(listener, serviceInfo); if (key == BUSY_LISTENER_KEY) { throw new IllegalArgumentException("listener already in use"); } mAsyncChannel.sendMessage(RESOLVE_SERVICE, 0, key, serviceInfo); } Loading @@ -664,10 +628,10 @@ public final class NsdManager { } /** * Get a reference to NetworkService handler. This is used to establish * Get a reference to NsdService handler. This is used to establish * an AsyncChannel communication with the service * * @return Messenger pointing to the NetworkService handler * @return Messenger pointing to the NsdService handler */ private Messenger getMessenger() { try { Loading @@ -676,4 +640,18 @@ public final class NsdManager { throw e.rethrowFromSystemServer(); } } private static void checkListener(Object listener) { checkNotNull(listener, "listener cannot be null"); } private static void checkProtocol(int protocolType) { checkArgument(protocolType == PROTOCOL_DNS_SD, "Unsupported protocol"); } private static void checkServiceInfo(NsdServiceInfo serviceInfo) { checkNotNull(serviceInfo, "NsdServiceInfo cannot be null"); checkStringNotEmpty(serviceInfo.getServiceName(),"Service name cannot be empty"); checkStringNotEmpty(serviceInfo.getServiceType(), "Service type cannot be empty"); } }
services/core/java/com/android/server/NsdService.java +22 −39 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import android.provider.Settings; import android.util.Base64; import android.util.Slog; import android.util.SparseArray; import android.util.SparseIntArray; import java.io.FileDescriptor; import java.io.PrintWriter; Loading @@ -48,7 +49,6 @@ import com.android.internal.util.AsyncChannel; import com.android.internal.util.Protocol; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.server.NativeDaemonConnector.Command; /** * Network Service Discovery Service handles remote service discovery operation requests by Loading Loading @@ -161,7 +161,7 @@ public class NsdService extends INsdManager.Stub { } //Last client if (mClients.size() == 0) { stopMDnsDaemon(); mDaemon.stop(); } break; case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: Loading Loading @@ -221,14 +221,14 @@ public class NsdService extends INsdManager.Stub { public void enter() { sendNsdStateChangeBroadcast(true); if (mClients.size() > 0) { startMDnsDaemon(); mDaemon.start(); } } @Override public void exit() { if (mClients.size() > 0) { stopMDnsDaemon(); mDaemon.stop(); } } Loading @@ -247,8 +247,8 @@ public class NsdService extends INsdManager.Stub { } private void removeRequestMap(int clientId, int globalId, ClientInfo clientInfo) { clientInfo.mClientIds.remove(clientId); clientInfo.mClientRequests.remove(clientId); clientInfo.mClientIds.delete(clientId); clientInfo.mClientRequests.delete(clientId); mIdToClientInfoMap.remove(globalId); } Loading @@ -262,7 +262,7 @@ public class NsdService extends INsdManager.Stub { //First client if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL && mClients.size() == 0) { startMDnsDaemon(); mDaemon.start(); } return NOT_HANDLED; case AsyncChannel.CMD_CHANNEL_DISCONNECTED: Loading Loading @@ -301,7 +301,7 @@ public class NsdService extends INsdManager.Stub { clientInfo = mClients.get(msg.replyTo); try { id = clientInfo.mClientIds.get(msg.arg2).intValue(); id = clientInfo.mClientIds.get(msg.arg2); } catch (NullPointerException e) { replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED, NsdManager.FAILURE_INTERNAL_ERROR); Loading Loading @@ -339,7 +339,7 @@ public class NsdService extends INsdManager.Stub { if (DBG) Slog.d(TAG, "unregister service"); clientInfo = mClients.get(msg.replyTo); try { id = clientInfo.mClientIds.get(msg.arg2).intValue(); id = clientInfo.mClientIds.get(msg.arg2); } catch (NullPointerException e) { replyToMessage(msg, NsdManager.UNREGISTER_SERVICE_FAILED, NsdManager.FAILURE_INTERNAL_ERROR); Loading Loading @@ -712,26 +712,13 @@ public class NsdService extends INsdManager.Stub { return true; } public boolean execute(Command cmd) { if (DBG) { Slog.d(TAG, cmd.toString()); } try { mNativeConnector.execute(cmd); } catch (NativeDaemonConnectorException e) { Slog.e(TAG, "Failed to execute " + cmd, e); return false; } return true; } public void start() { execute("start-service"); } private boolean startMDnsDaemon() { return mDaemon.execute("start-service"); public void stop() { execute("stop-service"); } private boolean stopMDnsDaemon() { return mDaemon.execute("stop-service"); } private boolean registerService(int regId, NsdServiceInfo service) { Loading @@ -743,8 +730,7 @@ public class NsdService extends INsdManager.Stub { int port = service.getPort(); byte[] textRecord = service.getTxtRecord(); String record = Base64.encodeToString(textRecord, Base64.DEFAULT).replace("\n", ""); Command cmd = new Command("mdnssd", "register", regId, name, type, port, record); return mDaemon.execute(cmd); return mDaemon.execute("register", regId, name, type, port, record); } private boolean unregisterService(int regId) { Loading Loading @@ -843,10 +829,10 @@ public class NsdService extends INsdManager.Stub { private NsdServiceInfo mResolvedService; /* A map from client id to unique id sent to mDns */ private final SparseArray<Integer> mClientIds = new SparseArray<>(); private final SparseIntArray mClientIds = new SparseIntArray(); /* A map from client id to the type of the request we had received */ private final SparseArray<Integer> mClientRequests = new SparseArray<>(); private final SparseIntArray mClientRequests = new SparseIntArray(); private ClientInfo(AsyncChannel c, Messenger m) { mChannel = c; Loading @@ -873,6 +859,7 @@ public class NsdService extends INsdManager.Stub { // and send cancellations to the daemon. private void expungeAllRequests() { int globalId, clientId, i; // TODO: to keep handler responsive, do not clean all requests for that client at once. for (i = 0; i < mClientIds.size(); i++) { clientId = mClientIds.keyAt(i); globalId = mClientIds.valueAt(i); Loading Loading @@ -900,15 +887,11 @@ public class NsdService extends INsdManager.Stub { // mClientIds is a sparse array of listener id -> mDnsClient id. For a given mDnsClient id, // return the corresponding listener id. mDnsClient id is also called a global id. private int getClientId(final int globalId) { // This doesn't use mClientIds.indexOfValue because indexOfValue uses == (not .equals) // while also coercing the int primitives to Integer objects. for (int i = 0, nSize = mClientIds.size(); i < nSize; i++) { int mDnsId = mClientIds.valueAt(i); if (globalId == mDnsId) { return mClientIds.keyAt(i); } int idx = mClientIds.indexOfValue(globalId); if (idx < 0) { return idx; } return -1; return mClientIds.keyAt(idx); } } Loading
tests/net/java/android/net/nsd/NsdServiceTest.java +84 −13 Original line number Diff line number Diff line Loading @@ -16,68 +16,121 @@ package com.android.server; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.test.TestLooper; import android.content.Context; import android.content.ContentResolver; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import com.android.server.NsdService.DaemonConnection; import com.android.server.NsdService.DaemonConnectionSupplier; import com.android.server.NsdService.NativeCallbackReceiver; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; // TODOs: // - test client disconnects // - test client can send requests and receive replies // - test NSD_ON ENABLE/DISABLED listening @RunWith(AndroidJUnit4.class) @SmallTest public class NsdServiceTest { static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD; long mTimeoutMs = 100; // non-final so that tests can adjust the value. @Mock Context mContext; @Mock ContentResolver mResolver; @Mock NsdService.NsdSettings mSettings; @Mock DaemonConnection mDaemon; NativeCallbackReceiver mDaemonCallback; TestLooper mLooper; HandlerThread mThread; TestHandler mHandler; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mLooper = new TestLooper(); mHandler = new TestHandler(mLooper.getLooper()); mThread = new HandlerThread("mock-service-handler"); mThread.start(); mHandler = new TestHandler(mThread.getLooper()); when(mContext.getContentResolver()).thenReturn(mResolver); } @After public void tearDown() throws Exception { mThread.quit(); } @Test public void testClientsCanConnect() { public void testClientsCanConnectAndDisconnect() { when(mSettings.isEnabled()).thenReturn(true); NsdService service = makeService(); NsdManager client1 = connectClient(service); verify(mDaemon, timeout(100).times(1)).execute("start-service"); verify(mDaemon, timeout(100).times(1)).start(); NsdManager client2 = connectClient(service); // TODO: disconnect client1 // TODO: disconnect client2 client1.disconnect(); client2.disconnect(); verify(mDaemon, timeout(mTimeoutMs).times(1)).stop(); } @Test public void testClientRequestsAreGCedAtDisconnection() { when(mSettings.isEnabled()).thenReturn(true); when(mDaemon.execute(any())).thenReturn(true); NsdService service = makeService(); NsdManager client = connectClient(service); verify(mDaemon, timeout(100).times(1)).start(); NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); request.setPort(2201); // Client registration request NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); client.registerService(request, PROTOCOL, listener1); verifyDaemonCommand("register 2 a_name a_type 2201"); // Client discovery request NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class); client.discoverServices("a_type", PROTOCOL, listener2); verifyDaemonCommand("discover 3 a_type"); // Client resolve request NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class); client.resolveService(request, listener3); verifyDaemonCommand("resolve 4 a_name a_type local."); // Client disconnects client.disconnect(); verify(mDaemon, timeout(mTimeoutMs).times(1)).stop(); // checks that request are cleaned verifyDaemonCommands("stop-register 2", "stop-discover 3", "stop-resolve 4"); } NsdService makeService() { Loading @@ -91,10 +144,28 @@ public class NsdServiceTest { } NsdManager connectClient(NsdService service) { mLooper.startAutoDispatch(); NsdManager client = new NsdManager(mContext, service); mLooper.stopAutoDispatch(); return client; return new NsdManager(mContext, service); } void verifyDaemonCommands(String... wants) { verifyDaemonCommand(String.join(" ", wants), wants.length); } void verifyDaemonCommand(String want) { verifyDaemonCommand(want, 1); } void verifyDaemonCommand(String want, int n) { ArgumentCaptor<Object> argumentsCaptor = ArgumentCaptor.forClass(Object.class); verify(mDaemon, timeout(mTimeoutMs).times(n)).execute(argumentsCaptor.capture()); String got = ""; for (Object o : argumentsCaptor.getAllValues()) { got += o + " "; } assertEquals(want, got.trim()); // rearm deamon for next command verification reset(mDaemon); when(mDaemon.execute(any())).thenReturn(true); } public static class TestHandler extends Handler { Loading