Refactor NsdManagerTest
Refactor the test to use Kotlin, and move some repeated code to
submethods.
Bug: 190249673
Test: atest NsdManagerTest
Change-Id: Iee7273080d5d29f1d364ac0a77e017edf9b36051
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.java b/tests/cts/net/src/android/net/cts/NsdManagerTest.java
deleted file mode 100644
index 2bcfdc3..0000000
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.java
+++ /dev/null
@@ -1,594 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.cts;
-
-import android.content.Context;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.platform.test.annotations.AppModeFull;
-import android.test.AndroidTestCase;
-import android.util.Log;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.util.Arrays;
-import java.util.Random;
-import java.util.List;
-import java.util.ArrayList;
-
-@AppModeFull(reason = "Socket cannot bind in instant app mode")
-public class NsdManagerTest extends AndroidTestCase {
-
- private static final String TAG = "NsdManagerTest";
- private static final String SERVICE_TYPE = "_nmt._tcp";
- private static final int TIMEOUT = 2000;
-
- private static final boolean DBG = false;
-
- NsdManager mNsdManager;
-
- NsdManager.RegistrationListener mRegistrationListener;
- NsdManager.DiscoveryListener mDiscoveryListener;
- NsdManager.ResolveListener mResolveListener;
- private NsdServiceInfo mResolvedService;
-
- public NsdManagerTest() {
- initRegistrationListener();
- initDiscoveryListener();
- initResolveListener();
- }
-
- private void initRegistrationListener() {
- mRegistrationListener = new NsdManager.RegistrationListener() {
- @Override
- public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
- setEvent("onRegistrationFailed", errorCode);
- }
-
- @Override
- public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
- setEvent("onUnregistrationFailed", errorCode);
- }
-
- @Override
- public void onServiceRegistered(NsdServiceInfo serviceInfo) {
- setEvent("onServiceRegistered", serviceInfo);
- }
-
- @Override
- public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
- setEvent("onServiceUnregistered", serviceInfo);
- }
- };
- }
-
- private void initDiscoveryListener() {
- mDiscoveryListener = new NsdManager.DiscoveryListener() {
- @Override
- public void onStartDiscoveryFailed(String serviceType, int errorCode) {
- setEvent("onStartDiscoveryFailed", errorCode);
- }
-
- @Override
- public void onStopDiscoveryFailed(String serviceType, int errorCode) {
- setEvent("onStopDiscoveryFailed", errorCode);
- }
-
- @Override
- public void onDiscoveryStarted(String serviceType) {
- NsdServiceInfo info = new NsdServiceInfo();
- info.setServiceType(serviceType);
- setEvent("onDiscoveryStarted", info);
- }
-
- @Override
- public void onDiscoveryStopped(String serviceType) {
- NsdServiceInfo info = new NsdServiceInfo();
- info.setServiceType(serviceType);
- setEvent("onDiscoveryStopped", info);
- }
-
- @Override
- public void onServiceFound(NsdServiceInfo serviceInfo) {
- setEvent("onServiceFound", serviceInfo);
- }
-
- @Override
- public void onServiceLost(NsdServiceInfo serviceInfo) {
- setEvent("onServiceLost", serviceInfo);
- }
- };
- }
-
- private void initResolveListener() {
- mResolveListener = new NsdManager.ResolveListener() {
- @Override
- public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
- setEvent("onResolveFailed", errorCode);
- }
-
- @Override
- public void onServiceResolved(NsdServiceInfo serviceInfo) {
- mResolvedService = serviceInfo;
- setEvent("onServiceResolved", serviceInfo);
- }
- };
- }
-
-
-
- private final class EventData {
- EventData(String callbackName, NsdServiceInfo info) {
- mCallbackName = callbackName;
- mSucceeded = true;
- mErrorCode = 0;
- mInfo = info;
- }
- EventData(String callbackName, int errorCode) {
- mCallbackName = callbackName;
- mSucceeded = false;
- mErrorCode = errorCode;
- mInfo = null;
- }
- private final String mCallbackName;
- private final boolean mSucceeded;
- private final int mErrorCode;
- private final NsdServiceInfo mInfo;
- }
-
- private final List<EventData> mEventCache = new ArrayList<EventData>();
-
- private void setEvent(String callbackName, int errorCode) {
- if (DBG) Log.d(TAG, callbackName + " failed with " + String.valueOf(errorCode));
- EventData eventData = new EventData(callbackName, errorCode);
- synchronized (mEventCache) {
- mEventCache.add(eventData);
- mEventCache.notify();
- }
- }
-
- private void setEvent(String callbackName, NsdServiceInfo info) {
- if (DBG) Log.d(TAG, "Received event " + callbackName + " for " + info.getServiceName());
- EventData eventData = new EventData(callbackName, info);
- synchronized (mEventCache) {
- mEventCache.add(eventData);
- mEventCache.notify();
- }
- }
-
- void clearEventCache() {
- synchronized(mEventCache) {
- mEventCache.clear();
- }
- }
-
- int eventCacheSize() {
- synchronized(mEventCache) {
- return mEventCache.size();
- }
- }
-
- private int mWaitId = 0;
- private EventData waitForCallback(String callbackName) {
-
- synchronized(mEventCache) {
-
- mWaitId ++;
- if (DBG) Log.d(TAG, "Waiting for " + callbackName + ", id=" + String.valueOf(mWaitId));
-
- try {
- long startTime = android.os.SystemClock.uptimeMillis();
- long elapsedTime = 0;
- int index = 0;
- while (elapsedTime < TIMEOUT ) {
- // first check if we've received that event
- for (; index < mEventCache.size(); index++) {
- EventData e = mEventCache.get(index);
- if (e.mCallbackName.equals(callbackName)) {
- if (DBG) Log.d(TAG, "exiting wait id=" + String.valueOf(mWaitId));
- return e;
- }
- }
-
- // Not yet received, just wait
- mEventCache.wait(TIMEOUT - elapsedTime);
- elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
- }
- // we exited the loop because of TIMEOUT; fail the call
- if (DBG) Log.d(TAG, "timed out waiting id=" + String.valueOf(mWaitId));
- return null;
- } catch (InterruptedException e) {
- return null; // wait timed out!
- }
- }
- }
-
- private EventData waitForNewEvents() throws InterruptedException {
- if (DBG) Log.d(TAG, "Waiting for a bit, id=" + String.valueOf(mWaitId));
-
- long startTime = android.os.SystemClock.uptimeMillis();
- long elapsedTime = 0;
- synchronized (mEventCache) {
- int index = mEventCache.size();
- while (elapsedTime < TIMEOUT ) {
- // first check if we've received that event
- for (; index < mEventCache.size(); index++) {
- EventData e = mEventCache.get(index);
- return e;
- }
-
- // Not yet received, just wait
- mEventCache.wait(TIMEOUT - elapsedTime);
- elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
- }
- }
-
- return null;
- }
-
- private String mServiceName;
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- if (DBG) Log.d(TAG, "Setup test ...");
- mNsdManager = (NsdManager) getContext().getSystemService(Context.NSD_SERVICE);
-
- Random rand = new Random();
- mServiceName = new String("NsdTest");
- for (int i = 0; i < 4; i++) {
- mServiceName = mServiceName + String.valueOf(rand.nextInt(10));
- }
- }
-
- @Override
- public void tearDown() throws Exception {
- if (DBG) Log.d(TAG, "Tear down test ...");
- super.tearDown();
- }
-
- public void testNDSManager() throws Exception {
- EventData lastEvent = null;
-
- if (DBG) Log.d(TAG, "Starting test ...");
-
- NsdServiceInfo si = new NsdServiceInfo();
- si.setServiceType(SERVICE_TYPE);
- si.setServiceName(mServiceName);
-
- byte testByteArray[] = new byte[] {-128, 127, 2, 1, 0, 1, 2};
- String String256 = "1_________2_________3_________4_________5_________6_________" +
- "7_________8_________9_________10________11________12________13________" +
- "14________15________16________17________18________19________20________" +
- "21________22________23________24________25________123456";
-
- // Illegal attributes
- try {
- si.setAttribute(null, (String) null);
- fail("Could set null key");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute("", (String) null);
- fail("Could set empty key");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute(String256, (String) null);
- fail("Could set key with 255 characters");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute("key", String256.substring(3));
- fail("Could set key+value combination with more than 255 characters");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute("key", String256.substring(4));
- fail("Could set key+value combination with 255 characters");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute(new String(new byte[]{0x19}), (String) null);
- fail("Could set key with invalid character");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute("=", (String) null);
- fail("Could set key with invalid character");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- try {
- si.setAttribute(new String(new byte[]{0x7F}), (String) null);
- fail("Could set key with invalid character");
- } catch (IllegalArgumentException e) {
- // expected
- }
-
- // Allowed attributes
- si.setAttribute("booleanAttr", (String) null);
- si.setAttribute("keyValueAttr", "value");
- si.setAttribute("keyEqualsAttr", "=");
- si.setAttribute(" whiteSpaceKeyValueAttr ", " value ");
- si.setAttribute("binaryDataAttr", testByteArray);
- si.setAttribute("nullBinaryDataAttr", (byte[]) null);
- si.setAttribute("emptyBinaryDataAttr", new byte[]{});
- si.setAttribute("longkey", String256.substring(9));
-
- ServerSocket socket;
- int localPort;
-
- try {
- socket = new ServerSocket(0);
- localPort = socket.getLocalPort();
- si.setPort(localPort);
- } catch (IOException e) {
- if (DBG) Log.d(TAG, "Could not open a local socket");
- assertTrue(false);
- return;
- }
-
- if (DBG) Log.d(TAG, "Port = " + String.valueOf(localPort));
-
- clearEventCache();
-
- mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
- lastEvent = waitForCallback("onServiceRegistered"); // id = 1
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
- assertTrue(eventCacheSize() == 1);
-
- // We may not always get the name that we tried to register;
- // This events tells us the name that was registered.
- String registeredName = lastEvent.mInfo.getServiceName();
- si.setServiceName(registeredName);
-
- clearEventCache();
-
- mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
- mDiscoveryListener);
-
- // Expect discovery started
- lastEvent = waitForCallback("onDiscoveryStarted"); // id = 2
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- // Remove this event, so accounting becomes easier later
- synchronized (mEventCache) {
- mEventCache.remove(lastEvent);
- }
-
- // Expect a service record to be discovered (and filter the ones
- // that are unrelated to this test)
- boolean found = false;
- for (int i = 0; i < 32; i++) {
-
- lastEvent = waitForCallback("onServiceFound"); // id = 3
- if (lastEvent == null) {
- // no more onServiceFound events are being reported!
- break;
- }
-
- assertTrue(lastEvent.mSucceeded);
-
- if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
- lastEvent.mInfo.getServiceName());
-
- if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
- // Save it, as it will get overwritten with new serviceFound events
- si = lastEvent.mInfo;
- found = true;
- }
-
- // Remove this event from the event cache, so it won't be found by subsequent
- // calls to waitForCallback
- synchronized (mEventCache) {
- mEventCache.remove(lastEvent);
- }
- }
-
- assertTrue(found);
-
- // We've removed all serviceFound events, and we've removed the discoveryStarted
- // event as well, so now the event cache should be empty!
- assertTrue(eventCacheSize() == 0);
-
- // Resolve the service
- clearEventCache();
- mNsdManager.resolveService(si, mResolveListener);
- lastEvent = waitForCallback("onServiceResolved"); // id = 4
-
- assertNotNull(mResolvedService);
-
- // Check Txt attributes
- assertEquals(8, mResolvedService.getAttributes().size());
- assertTrue(mResolvedService.getAttributes().containsKey("booleanAttr"));
- assertNull(mResolvedService.getAttributes().get("booleanAttr"));
- assertEquals("value", new String(mResolvedService.getAttributes().get("keyValueAttr")));
- assertEquals("=", new String(mResolvedService.getAttributes().get("keyEqualsAttr")));
- assertEquals(" value ", new String(mResolvedService.getAttributes()
- .get(" whiteSpaceKeyValueAttr ")));
- assertEquals(String256.substring(9), new String(mResolvedService.getAttributes()
- .get("longkey")));
- assertTrue(Arrays.equals(testByteArray,
- mResolvedService.getAttributes().get("binaryDataAttr")));
- assertTrue(mResolvedService.getAttributes().containsKey("nullBinaryDataAttr"));
- assertNull(mResolvedService.getAttributes().get("nullBinaryDataAttr"));
- assertTrue(mResolvedService.getAttributes().containsKey("emptyBinaryDataAttr"));
- assertNull(mResolvedService.getAttributes().get("emptyBinaryDataAttr"));
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": Port = " +
- String.valueOf(lastEvent.mInfo.getPort()));
-
- assertTrue(lastEvent.mInfo.getPort() == localPort);
- assertTrue(eventCacheSize() == 1);
-
- checkForAdditionalEvents();
- clearEventCache();
-
- // Unregister the service
- mNsdManager.unregisterService(mRegistrationListener);
- lastEvent = waitForCallback("onServiceUnregistered"); // id = 5
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- // Expect a callback for service lost
- lastEvent = waitForCallback("onServiceLost"); // id = 6
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
-
- // Register service again to see if we discover it
- checkForAdditionalEvents();
- clearEventCache();
-
- si = new NsdServiceInfo();
- si.setServiceType(SERVICE_TYPE);
- si.setServiceName(mServiceName);
- si.setPort(localPort);
-
- // Create a new registration listener and register same service again
- initRegistrationListener();
-
- mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
-
- lastEvent = waitForCallback("onServiceRegistered"); // id = 7
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- registeredName = lastEvent.mInfo.getServiceName();
-
- // Expect a record to be discovered
- // Expect a service record to be discovered (and filter the ones
- // that are unrelated to this test)
- found = false;
- for (int i = 0; i < 32; i++) {
-
- lastEvent = waitForCallback("onServiceFound"); // id = 8
- if (lastEvent == null) {
- // no more onServiceFound events are being reported!
- break;
- }
-
- assertTrue(lastEvent.mSucceeded);
-
- if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
- lastEvent.mInfo.getServiceName());
-
- if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
- // Save it, as it will get overwritten with new serviceFound events
- si = lastEvent.mInfo;
- found = true;
- }
-
- // Remove this event from the event cache, so it won't be found by subsequent
- // calls to waitForCallback
- synchronized (mEventCache) {
- mEventCache.remove(lastEvent);
- }
- }
-
- assertTrue(found);
-
- // Resolve the service
- clearEventCache();
- mNsdManager.resolveService(si, mResolveListener);
- lastEvent = waitForCallback("onServiceResolved"); // id = 9
-
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
-
- if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
- lastEvent.mInfo.getServiceName());
-
- assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
-
- assertNotNull(mResolvedService);
-
- // Check that we don't have any TXT records
- assertEquals(0, mResolvedService.getAttributes().size());
-
- checkForAdditionalEvents();
- clearEventCache();
-
- mNsdManager.stopServiceDiscovery(mDiscoveryListener);
- lastEvent = waitForCallback("onDiscoveryStopped"); // id = 10
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
- assertTrue(checkCacheSize(1));
-
- checkForAdditionalEvents();
- clearEventCache();
-
- mNsdManager.unregisterService(mRegistrationListener);
-
- lastEvent = waitForCallback("onServiceUnregistered"); // id = 11
- assertTrue(lastEvent != null);
- assertTrue(lastEvent.mSucceeded);
- assertTrue(checkCacheSize(1));
- }
-
- boolean checkCacheSize(int size) {
- synchronized (mEventCache) {
- int cacheSize = mEventCache.size();
- if (cacheSize != size) {
- Log.d(TAG, "id = " + mWaitId + ": event cache size = " + cacheSize);
- for (int i = 0; i < cacheSize; i++) {
- EventData e = mEventCache.get(i);
- String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
- Log.d(TAG, "eventName is " + e.mCallbackName + sname);
- }
- }
- return (cacheSize == size);
- }
- }
-
- boolean checkForAdditionalEvents() {
- try {
- EventData e = waitForNewEvents();
- if (e != null) {
- String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
- Log.d(TAG, "ignoring unexpected event " + e.mCallbackName + sname);
- }
- return (e == null);
- }
- catch (InterruptedException ex) {
- return false;
- }
- }
-}
-
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
new file mode 100644
index 0000000..8daf720
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.cts
+
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdManager.DiscoveryListener
+import android.net.nsd.NsdManager.ResolveListener
+import android.net.nsd.NsdServiceInfo
+import android.os.SystemClock
+import android.platform.test.annotations.AppModeFull
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.ServerSocket
+import java.nio.charset.StandardCharsets
+import java.util.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.fail
+
+private const val TAG = "NsdManagerTest"
+private const val SERVICE_TYPE = "_nmt._tcp"
+private const val TIMEOUT = 2000
+private const val DBG = false
+
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+@RunWith(AndroidJUnit4::class)
+class NsdManagerTest {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
+ private val serviceName = "NsdTest%04d".format(Random().nextInt(1000))
+
+ private val registrationListener = object : NsdManager.RegistrationListener {
+ override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
+ setEvent("onRegistrationFailed", errorCode)
+ }
+
+ override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
+ setEvent("onUnregistrationFailed", errorCode)
+ }
+
+ override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
+ setEvent("onServiceRegistered", serviceInfo)
+ }
+
+ override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) {
+ setEvent("onServiceUnregistered", serviceInfo)
+ }
+ }
+
+ private val discoveryListener = object : DiscoveryListener {
+ override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
+ setEvent("onStartDiscoveryFailed", errorCode)
+ }
+
+ override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
+ setEvent("onStopDiscoveryFailed", errorCode)
+ }
+
+ override fun onDiscoveryStarted(serviceType: String) {
+ val info = NsdServiceInfo()
+ info.serviceType = serviceType
+ setEvent("onDiscoveryStarted", info)
+ }
+
+ override fun onDiscoveryStopped(serviceType: String) {
+ val info = NsdServiceInfo()
+ info.serviceType = serviceType
+ setEvent("onDiscoveryStopped", info)
+ }
+
+ override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+ setEvent("onServiceFound", serviceInfo)
+ }
+
+ override fun onServiceLost(serviceInfo: NsdServiceInfo) {
+ setEvent("onServiceLost", serviceInfo)
+ }
+ }
+
+ private inner class TestResolveListener : ResolveListener {
+ var resolvedService: NsdServiceInfo? = null
+ override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
+ setEvent("onResolveFailed", errorCode)
+ }
+
+ override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
+ resolvedService = serviceInfo
+ setEvent("onServiceResolved", serviceInfo)
+ }
+ }
+
+ private class EventData {
+ constructor(callbackName: String, info: NsdServiceInfo?) {
+ this.callbackName = callbackName
+ succeeded = true
+ errorCode = 0
+ this.info = info
+ }
+
+ constructor(callbackName: String, errorCode: Int) {
+ this.callbackName = callbackName
+ succeeded = false
+ this.errorCode = errorCode
+ info = null
+ }
+
+ val callbackName: String
+ val succeeded: Boolean
+ private val errorCode: Int
+ val info: NsdServiceInfo?
+ }
+
+ private val eventCache = ArrayList<EventData>()
+ private fun setEvent(callbackName: String, errorCode: Int) {
+ if (DBG) Log.d(TAG, "$callbackName failed with $errorCode")
+ val eventData = EventData(callbackName, errorCode)
+ synchronized(eventCache) {
+ eventCache.add(eventData)
+ eventCache.notify()
+ }
+ }
+
+ private fun setEvent(callbackName: String, info: NsdServiceInfo) {
+ if (DBG) Log.d(TAG, "Received event " + callbackName + " for " + info.serviceName)
+ val eventData = EventData(callbackName, info)
+ synchronized(eventCache) {
+ eventCache.add(eventData)
+ eventCache.notify()
+ }
+ }
+
+ fun clearEventCache() {
+ synchronized(eventCache) { eventCache.clear() }
+ }
+
+ fun eventCacheSize(): Int {
+ synchronized(eventCache) { return eventCache.size }
+ }
+
+ private var waitId = 0
+ private fun waitForCallback(callbackName: String): EventData? {
+ synchronized(eventCache) {
+ waitId++
+ if (DBG) Log.d(TAG, "Waiting for $callbackName, id=$waitId")
+ val startTime = SystemClock.uptimeMillis()
+ var elapsedTime = 0L
+ while (elapsedTime < TIMEOUT) {
+ // first check if we've received that event
+ eventCache.find { it.callbackName == callbackName }?.let {
+ if (DBG) Log.d(TAG, "exiting wait id=$waitId")
+ return it
+ }
+
+ // Not yet received, just wait
+ try {
+ eventCache.wait(TIMEOUT - elapsedTime)
+ } catch (e: InterruptedException) {
+ return null
+ }
+ elapsedTime = SystemClock.uptimeMillis() - startTime
+ }
+ // we exited the loop because of TIMEOUT; fail the call
+ if (DBG) Log.d(TAG, "timed out waiting id=$waitId")
+ return null
+ }
+ }
+
+ private fun waitForNewEvents(): EventData? {
+ if (DBG) Log.d(TAG, "Waiting for a bit, id=$waitId")
+ val startTime = SystemClock.uptimeMillis()
+ var elapsedTime = 0L
+ synchronized(eventCache) {
+ val index = eventCache.size
+ while (elapsedTime < TIMEOUT) {
+ // first check if we've received that event
+ if (index < eventCache.size) {
+ return eventCache[index]
+ }
+
+ // Not yet received, just wait
+ eventCache.wait(TIMEOUT - elapsedTime)
+ elapsedTime = SystemClock.uptimeMillis() - startTime
+ }
+ }
+ return null
+ }
+
+ @Test
+ fun testNsdManager() {
+ val si = NsdServiceInfo()
+ si.serviceType = SERVICE_TYPE
+ si.serviceName = serviceName
+ val testByteArray = byteArrayOf(-128, 127, 2, 1, 0, 1, 2)
+ val string256 = "1_________2_________3_________4_________5_________6_________" +
+ "7_________8_________9_________10________11________12________13________" +
+ "14________15________16________17________18________19________20________" +
+ "21________22________23________24________25________123456"
+
+ // Illegal attributes
+ assertFailsWith<IllegalArgumentException>("Could set null key") {
+ si.setAttribute(null, null as String?)
+ }
+ assertFailsWith<IllegalArgumentException>("Could set empty key") {
+ si.setAttribute("", null as String?)
+ }
+ assertFailsWith<IllegalArgumentException>("Could set key with 255 characters") {
+ si.setAttribute(string256, null as String?)
+ }
+ assertFailsWith<IllegalArgumentException>(
+ "Could set key+value combination with more than 255 characters") {
+ si.setAttribute("key", string256.substring(3))
+ }
+ assertFailsWith<IllegalArgumentException>(
+ "Could set key+value combination with 255 characters") {
+ si.setAttribute("key", string256.substring(4))
+ }
+ assertFailsWith<IllegalArgumentException>("Could set key with invalid character") {
+ si.setAttribute("\u0019", null as String?)
+ }
+ assertFailsWith<IllegalArgumentException>("Could set key with invalid character") {
+ si.setAttribute("=", null as String?)
+ }
+ assertFailsWith<IllegalArgumentException>("Could set key with invalid character") {
+ si.setAttribute("\u007f", null as String?)
+ }
+
+ // Allowed attributes
+ si.setAttribute("booleanAttr", null as String?)
+ si.setAttribute("keyValueAttr", "value")
+ si.setAttribute("keyEqualsAttr", "=")
+ si.setAttribute(" whiteSpaceKeyValueAttr ", " value ")
+ si.setAttribute("binaryDataAttr", testByteArray)
+ si.setAttribute("nullBinaryDataAttr", null as ByteArray?)
+ si.setAttribute("emptyBinaryDataAttr", byteArrayOf())
+ si.setAttribute("longkey", string256.substring(9))
+ val socket = ServerSocket(0)
+ val localPort = socket.localPort
+ si.port = localPort
+ if (DBG) Log.d(TAG, "Port = $localPort")
+ clearEventCache()
+
+ val registeredName = registerService(si)
+
+ assertEquals(1, eventCacheSize())
+ clearEventCache()
+ nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
+
+ // Expect discovery started
+ var lastEvent = waitForCallback("onDiscoveryStarted")
+ assertNotNull(lastEvent)
+ assertTrue(lastEvent.succeeded)
+
+ // Remove this event, so accounting becomes easier later
+ synchronized(eventCache) { eventCache.remove(lastEvent) }
+
+ // Expect a service record to be discovered
+ val foundInfo = waitForServiceDiscovered(registeredName)
+
+ // We've removed all serviceFound events, and we've removed the discoveryStarted
+ // event as well, so now the event cache should be empty!
+ assertEquals(0, eventCacheSize())
+
+ val resolvedService = resolveService(foundInfo)
+
+ // Check Txt attributes
+ assertEquals(8, resolvedService.attributes.size)
+ assertTrue(resolvedService.attributes.containsKey("booleanAttr"))
+ assertNull(resolvedService.attributes["booleanAttr"])
+ assertEquals("value", resolvedService.attributes["keyValueAttr"].utf8ToString())
+ assertEquals("=", resolvedService.attributes["keyEqualsAttr"].utf8ToString())
+ assertEquals(" value ",
+ resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString())
+ assertEquals(string256.substring(9), resolvedService.attributes["longkey"].utf8ToString())
+ assertArrayEquals(testByteArray, resolvedService.attributes["binaryDataAttr"])
+ assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
+ assertNull(resolvedService.attributes["nullBinaryDataAttr"])
+ assertTrue(resolvedService.attributes.containsKey("emptyBinaryDataAttr"))
+ assertNull(resolvedService.attributes["emptyBinaryDataAttr"])
+ if (DBG) Log.d(TAG, "id = $waitId: Port = ${lastEvent.info?.port}")
+ assertEquals(localPort, resolvedService.port)
+ assertEquals(1, eventCacheSize())
+ checkForAdditionalEvents()
+ clearEventCache()
+
+ // Unregister the service
+ nsdManager.unregisterService(registrationListener)
+ lastEvent = waitForCallback("onServiceUnregistered")
+ assertNotNull(lastEvent)
+ assertTrue(lastEvent.succeeded)
+
+ // Expect a callback for service lost
+ lastEvent = waitForCallback("onServiceLost")
+ assertNotNull(lastEvent)
+ assertEquals(registeredName, lastEvent.info?.serviceName)
+
+ // Register service again to see if we discover it
+ checkForAdditionalEvents()
+ clearEventCache()
+ val si2 = NsdServiceInfo()
+ si2.serviceType = SERVICE_TYPE
+ si2.serviceName = serviceName
+ si2.port = localPort
+ val registeredName2 = registerService(si2)
+
+ // Expect a record to be discovered
+ // Expect a service record to be discovered (and filter the ones
+ // that are unrelated to this test)
+ val foundInfo2 = waitForServiceDiscovered(registeredName2)
+
+ // Resolve the service
+ clearEventCache()
+ val resolvedService2 = resolveService(foundInfo2)
+
+ // Check that we don't have any TXT records
+ assertEquals(0, resolvedService2.attributes.size)
+ checkForAdditionalEvents()
+ clearEventCache()
+ nsdManager.stopServiceDiscovery(discoveryListener)
+ lastEvent = waitForCallback("onDiscoveryStopped")
+ assertNotNull(lastEvent)
+ assertTrue(lastEvent.succeeded)
+ checkCacheSize(1)
+ checkForAdditionalEvents()
+ clearEventCache()
+ nsdManager.unregisterService(registrationListener)
+ lastEvent = waitForCallback("onServiceUnregistered")
+ assertNotNull(lastEvent)
+ assertTrue(lastEvent.succeeded)
+ checkCacheSize(1)
+ }
+
+ /**
+ * Register a service and return its registered name.
+ */
+ private fun registerService(si: NsdServiceInfo): String {
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationListener)
+
+ // We may not always get the name that we tried to register;
+ // This events tells us the name that was registered.
+ val cb = waitForCallback("onServiceRegistered")
+ assertNotNull(cb)
+ assertTrue(cb.succeeded)
+ return cb.info?.serviceName ?: fail("Missing event info")
+ }
+
+ private fun waitForServiceDiscovered(serviceName: String): NsdServiceInfo {
+ var foundInfo: NsdServiceInfo? = null
+ repeat(32) {
+ val event = waitForCallback("onServiceFound") ?: return@repeat
+ assertTrue(event.succeeded)
+ if (DBG) Log.d(TAG, "id = $waitId: ServiceName = ${event.info?.serviceName}")
+ if (event.info?.serviceName == serviceName) {
+ // Save it, as it will get overwritten with new serviceFound events
+ foundInfo = event.info
+ }
+
+ // Remove this event from the event cache, so it won't be found by subsequent
+ // calls to waitForCallback
+ synchronized(eventCache) { eventCache.remove(event) }
+ }
+ return foundInfo ?: fail("Service not discovered")
+ }
+
+ private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
+ val resolveListener = TestResolveListener()
+ nsdManager.resolveService(discoveredInfo, resolveListener)
+ val resolvedCb = waitForCallback("onServiceResolved")
+ assertNotNull(resolvedCb)
+ assertTrue(resolvedCb.succeeded)
+ if (DBG) Log.d(TAG, "id = $waitId: ServiceName = ${resolvedCb.info?.serviceName}")
+ assertEquals(discoveredInfo.serviceName, resolvedCb.info?.serviceName)
+
+ return resolveListener.resolvedService ?: fail("Missing resolved service")
+ }
+
+ fun checkCacheSize(size: Int) {
+ synchronized(eventCache) {
+ if (size != eventCache.size) {
+ fail("Expected size $size, found event list [${
+ eventCache.joinToString(", ") {
+ "eventName: ${it.callbackName}, serviceName ${it.info?.serviceName}"
+ }
+ }]")
+ }
+ }
+ }
+
+ fun checkForAdditionalEvents(): Boolean {
+ val e = waitForNewEvents() ?: return true
+ Log.d(TAG, "ignoring unexpected event ${e.callbackName} (${e.info?.serviceName})")
+ return false
+ }
+}
+
+private fun ByteArray?.utf8ToString(): String {
+ if (this == null) return ""
+ return String(this, StandardCharsets.UTF_8)
+}
+
+// TODO: migrate legacy java-style implementation to newer utils like RecorderCallback
+private fun Any.wait(timeout: Long) = (this as Object).wait(timeout)
+private fun Any.notify() = (this as Object).notify()
\ No newline at end of file