Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fa11edc7 authored by Android Build Merger (Role)'s avatar Android Build Merger (Role) Committed by Android (Google) Code Review
Browse files

Merge "Merge "Move some NetworkStats tests to tests/net." into oc-dev am:...

Merge "Merge "Move some NetworkStats tests to tests/net." into oc-dev am: 74fe2cbc  -s ours am: ea28d269  -s ours"
parents 5c173f15 81f1cc1c
Loading
Loading
Loading
Loading
+103 −81
Original line number Diff line number Diff line
@@ -16,10 +16,6 @@

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;
@@ -245,12 +241,12 @@ public final class NsdManager {
        return name;
    }

    private static int FIRST_LISTENER_KEY = 1;

    private final INsdManager mService;
    private final Context mContext;

    private int mListenerKey = FIRST_LISTENER_KEY;
    private static final int INVALID_LISTENER_KEY = 0;
    private static final int BUSY_LISTENER_KEY = -1;
    private int mListenerKey = 1;
    private final SparseArray mListenerMap = new SparseArray();
    private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>();
    private final Object mMapLock = new Object();
@@ -273,14 +269,6 @@ public final class NsdManager {
        init();
    }

    /**
     * @hide
     */
    @VisibleForTesting
    public void disconnect() {
        mAsyncChannel.disconnect();
    }

    /**
     * Failures are passed with {@link RegistrationListener#onRegistrationFailed},
     * {@link RegistrationListener#onUnregistrationFailed},
@@ -316,6 +304,7 @@ public final class NsdManager {
        public void onServiceFound(NsdServiceInfo serviceInfo);

        public void onServiceLost(NsdServiceInfo serviceInfo);

    }

    /** Interface for callback invocation for service registration */
@@ -346,9 +335,8 @@ public final class NsdManager {

        @Override
        public void handleMessage(Message message) {
            final int what = message.what;
            final int key = message.arg2;
            switch (what) {
            if (DBG) Log.d(TAG, "received " + nameOf(message.what));
            switch (message.what) {
                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
                    mAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
                    return;
@@ -361,26 +349,19 @@ public final class NsdManager {
                default:
                    break;
            }
            final Object listener;
            final NsdServiceInfo ns;
            synchronized (mMapLock) {
                listener = mListenerMap.get(key);
                ns = mServiceMap.get(key);
            }
            Object listener = getListener(message.arg2);
            if (listener == null) {
                Log.d(TAG, "Stale key " + message.arg2);
                return;
            }
            if (DBG) {
                Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
            }
            switch (what) {
            NsdServiceInfo ns = getNsdService(message.arg2);
            switch (message.what) {
                case DISCOVER_SERVICES_STARTED:
                    String s = getNsdServiceInfoType((NsdServiceInfo) message.obj);
                    ((DiscoveryListener) listener).onDiscoveryStarted(s);
                    break;
                case DISCOVER_SERVICES_FAILED:
                    removeListener(key);
                    removeListener(message.arg2);
                    ((DiscoveryListener) listener).onStartDiscoveryFailed(getNsdServiceInfoType(ns),
                            message.arg1);
                    break;
@@ -393,16 +374,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(key);
                    removeListener(message.arg2);
                    ((DiscoveryListener) listener).onStopDiscoveryFailed(getNsdServiceInfoType(ns),
                            message.arg1);
                    break;
                case STOP_DISCOVERY_SUCCEEDED:
                    removeListener(key);
                    removeListener(message.arg2);
                    ((DiscoveryListener) listener).onDiscoveryStopped(getNsdServiceInfoType(ns));
                    break;
                case REGISTER_SERVICE_FAILED:
                    removeListener(key);
                    removeListener(message.arg2);
                    ((RegistrationListener) listener).onRegistrationFailed(ns, message.arg1);
                    break;
                case REGISTER_SERVICE_SUCCEEDED:
@@ -410,7 +391,7 @@ public final class NsdManager {
                            (NsdServiceInfo) message.obj);
                    break;
                case UNREGISTER_SERVICE_FAILED:
                    removeListener(key);
                    removeListener(message.arg2);
                    ((RegistrationListener) listener).onUnregistrationFailed(ns, message.arg1);
                    break;
                case UNREGISTER_SERVICE_SUCCEEDED:
@@ -420,11 +401,11 @@ public final class NsdManager {
                    ((RegistrationListener) listener).onServiceUnregistered(ns);
                    break;
                case RESOLVE_SERVICE_FAILED:
                    removeListener(key);
                    removeListener(message.arg2);
                    ((ResolveListener) listener).onResolveFailed(ns, message.arg1);
                    break;
                case RESOLVE_SERVICE_SUCCEEDED:
                    removeListener(key);
                    removeListener(message.arg2);
                    ((ResolveListener) listener).onServiceResolved((NsdServiceInfo) message.obj);
                    break;
                default:
@@ -434,27 +415,40 @@ public final class NsdManager {
        }
    }

    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
    // if the listener is already in the map, reject it.  Otherwise, add it and
    // return its key.
    private int putListener(Object listener, NsdServiceInfo s) {
        checkListener(listener);
        final int key;
        if (listener == null) return INVALID_LISTENER_KEY;
        int key;
        synchronized (mMapLock) {
            int valueIndex = mListenerMap.indexOfValue(listener);
            checkArgument(valueIndex == -1, "listener already in use");
            key = nextListenerKey();
            if (valueIndex != -1) {
                return BUSY_LISTENER_KEY;
            }
            do {
                key = mListenerKey++;
            } while (key == INVALID_LISTENER_KEY);
            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);
@@ -462,15 +456,16 @@ public final class NsdManager {
    }

    private int getListenerKey(Object listener) {
        checkListener(listener);
        synchronized (mMapLock) {
            int valueIndex = mListenerMap.indexOfValue(listener);
            checkArgument(valueIndex != -1, "listener not registered");
            if (valueIndex != -1) {
                return mListenerMap.keyAt(valueIndex);
            }
        }
        return INVALID_LISTENER_KEY;
    }

    private static String getNsdServiceInfoType(NsdServiceInfo s) {
    private String getNsdServiceInfoType(NsdServiceInfo s) {
        if (s == null) return "?";
        return s.getServiceType();
    }
@@ -480,9 +475,7 @@ public final class NsdManager {
     */
    private void init() {
        final Messenger messenger = getMessenger();
        if (messenger == null) {
            fatal("Failed to obtain service Messenger");
        }
        if (messenger == null) throw new RuntimeException("Failed to initialize");
        HandlerThread t = new HandlerThread("NsdManager");
        t.start();
        mHandler = new ServiceHandler(t.getLooper());
@@ -490,13 +483,8 @@ public final class NsdManager {
        try {
            mConnected.await();
        } catch (InterruptedException e) {
            fatal("Interrupted wait at init");
        }
            Log.e(TAG, "interrupted wait at init");
        }

    private static void fatal(String msg) {
        Log.e(TAG, msg);
        throw new RuntimeException(msg);
    }

    /**
@@ -518,10 +506,23 @@ public final class NsdManager {
     */
    public void registerService(NsdServiceInfo serviceInfo, int protocolType,
            RegistrationListener listener) {
        checkArgument(serviceInfo.getPort() > 0, "Invalid port number");
        checkServiceInfo(serviceInfo);
        checkProtocol(protocolType);
        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");
        }
        int key = putListener(listener, serviceInfo);
        if (key == BUSY_LISTENER_KEY) {
            throw new IllegalArgumentException("listener already in use");
        }
        mAsyncChannel.sendMessage(REGISTER_SERVICE, 0, key, serviceInfo);
    }

@@ -540,6 +541,12 @@ 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);
    }

@@ -572,13 +579,25 @@ 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) {
        checkStringNotEmpty(serviceType, "Service type cannot be empty");
        checkProtocol(protocolType);
        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");
        }

        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);
    }

@@ -600,6 +619,12 @@ 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);
    }

@@ -613,8 +638,19 @@ public final class NsdManager {
     * Cannot be in use for an active service resolution.
     */
    public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
        checkServiceInfo(serviceInfo);
        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");
        }

        int key = putListener(listener, serviceInfo);

        if (key == BUSY_LISTENER_KEY) {
            throw new IllegalArgumentException("listener already in use");
        }
        mAsyncChannel.sendMessage(RESOLVE_SERVICE, 0, key, serviceInfo);
    }

@@ -628,10 +664,10 @@ public final class NsdManager {
    }

    /**
     * Get a reference to NsdService handler. This is used to establish
     * Get a reference to NetworkService handler. This is used to establish
     * an AsyncChannel communication with the service
     *
     * @return Messenger pointing to the NsdService handler
     * @return Messenger pointing to the NetworkService handler
     */
    private Messenger getMessenger() {
        try {
@@ -640,18 +676,4 @@ 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");
    }
}
+39 −22
Original line number Diff line number Diff line
@@ -35,7 +35,6 @@ 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;
@@ -50,6 +49,7 @@ import com.android.internal.util.DumpUtils;
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
@@ -162,7 +162,7 @@ public class NsdService extends INsdManager.Stub {
                        }
                        //Last client
                        if (mClients.size() == 0) {
                            mDaemon.stop();
                            stopMDnsDaemon();
                        }
                        break;
                    case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION:
@@ -222,14 +222,14 @@ public class NsdService extends INsdManager.Stub {
            public void enter() {
                sendNsdStateChangeBroadcast(true);
                if (mClients.size() > 0) {
                    mDaemon.start();
                    startMDnsDaemon();
                }
            }

            @Override
            public void exit() {
                if (mClients.size() > 0) {
                    mDaemon.stop();
                    stopMDnsDaemon();
                }
            }

@@ -248,8 +248,8 @@ public class NsdService extends INsdManager.Stub {
            }

            private void removeRequestMap(int clientId, int globalId, ClientInfo clientInfo) {
                clientInfo.mClientIds.delete(clientId);
                clientInfo.mClientRequests.delete(clientId);
                clientInfo.mClientIds.remove(clientId);
                clientInfo.mClientRequests.remove(clientId);
                mIdToClientInfoMap.remove(globalId);
            }

@@ -263,7 +263,7 @@ public class NsdService extends INsdManager.Stub {
                        //First client
                        if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL &&
                                mClients.size() == 0) {
                            mDaemon.start();
                            startMDnsDaemon();
                        }
                        return NOT_HANDLED;
                    case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
@@ -302,7 +302,7 @@ public class NsdService extends INsdManager.Stub {
                        clientInfo = mClients.get(msg.replyTo);

                        try {
                            id = clientInfo.mClientIds.get(msg.arg2);
                            id = clientInfo.mClientIds.get(msg.arg2).intValue();
                        } catch (NullPointerException e) {
                            replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED,
                                    NsdManager.FAILURE_INTERNAL_ERROR);
@@ -340,7 +340,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);
                            id = clientInfo.mClientIds.get(msg.arg2).intValue();
                        } catch (NullPointerException e) {
                            replyToMessage(msg, NsdManager.UNREGISTER_SERVICE_FAILED,
                                    NsdManager.FAILURE_INTERNAL_ERROR);
@@ -713,13 +713,26 @@ public class NsdService extends INsdManager.Stub {
            return true;
        }

        public void start() {
            execute("start-service");
        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 stop() {
            execute("stop-service");
    private boolean startMDnsDaemon() {
        return mDaemon.execute("start-service");
    }

    private boolean stopMDnsDaemon() {
        return mDaemon.execute("stop-service");
    }

    private boolean registerService(int regId, NsdServiceInfo service) {
@@ -731,7 +744,8 @@ public class NsdService extends INsdManager.Stub {
        int port = service.getPort();
        byte[] textRecord = service.getTxtRecord();
        String record = Base64.encodeToString(textRecord, Base64.DEFAULT).replace("\n", "");
        return mDaemon.execute("register", regId, name, type, port, record);
        Command cmd = new Command("mdnssd", "register", regId, name, type, port, record);
        return mDaemon.execute(cmd);
    }

    private boolean unregisterService(int regId) {
@@ -824,10 +838,10 @@ public class NsdService extends INsdManager.Stub {
        private NsdServiceInfo mResolvedService;

        /* A map from client id to unique id sent to mDns */
        private final SparseIntArray mClientIds = new SparseIntArray();
        private final SparseArray<Integer> mClientIds = new SparseArray<>();

        /* A map from client id to the type of the request we had received */
        private final SparseIntArray mClientRequests = new SparseIntArray();
        private final SparseArray<Integer> mClientRequests = new SparseArray<>();

        private ClientInfo(AsyncChannel c, Messenger m) {
            mChannel = c;
@@ -854,7 +868,6 @@ 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);
@@ -882,11 +895,15 @@ 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) {
            int idx = mClientIds.indexOfValue(globalId);
            if (idx < 0) {
                return idx;
            // 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);
                }
            }
            return mClientIds.keyAt(idx);
            return -1;
        }
    }

+13 −84
Original line number Diff line number Diff line
@@ -16,121 +16,68 @@

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;
    HandlerThread mThread;
    TestLooper mLooper;
    TestHandler mHandler;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mThread = new HandlerThread("mock-service-handler");
        mThread.start();
        mHandler = new TestHandler(mThread.getLooper());
        mLooper = new TestLooper();
        mHandler = new TestHandler(mLooper.getLooper());
        when(mContext.getContentResolver()).thenReturn(mResolver);
    }

    @After
    public void tearDown() throws Exception {
        mThread.quit();
    }

    @Test
    public void testClientsCanConnectAndDisconnect() {
    public void testClientsCanConnect() {
        when(mSettings.isEnabled()).thenReturn(true);

        NsdService service = makeService();

        NsdManager client1 = connectClient(service);
        verify(mDaemon, timeout(100).times(1)).start();
        verify(mDaemon, timeout(100).times(1)).execute("start-service");

        NsdManager client2 = connectClient(service);

        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");
        // TODO: disconnect client1
        // TODO: disconnect client2
    }

    NsdService makeService() {
@@ -144,28 +91,10 @@ public class NsdServiceTest {
    }

    NsdManager connectClient(NsdService service) {
        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);
        mLooper.startAutoDispatch();
        NsdManager client = new NsdManager(mContext, service);
        mLooper.stopAutoDispatch();
        return client;
    }

    public static class TestHandler extends Handler {