[mdns] support discovering and resolving services
Bug: 305866779
Test: atest ThreadNetworkIntegrationTests:android.net.thread.ServiceDiscoveryTest
Change-Id: I96ee5b835c90fa55136dbbde68d36bbf2044b420
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index 3c7a72b..72e3980 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -21,6 +21,7 @@
import android.annotation.NonNull;
import android.content.Context;
import android.net.InetAddresses;
+import android.net.nsd.DiscoveryRequest;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
@@ -31,15 +32,20 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
import com.android.server.thread.openthread.INsdPublisher;
+import com.android.server.thread.openthread.INsdResolveServiceCallback;
import com.android.server.thread.openthread.INsdStatusReceiver;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -66,6 +72,8 @@
private final Handler mHandler;
private final Executor mExecutor;
private final SparseArray<RegistrationListener> mRegistrationListeners = new SparseArray<>(0);
+ private final SparseArray<DiscoveryListener> mDiscoveryListeners = new SparseArray<>(0);
+ private final SparseArray<ServiceInfoListener> mServiceInfoListeners = new SparseArray<>(0);
private final Deque<Runnable> mRegistrationJobs = new ArrayDeque<>();
@VisibleForTesting
@@ -197,6 +205,110 @@
mNsdManager.unregisterService(registrationListener);
}
+ @Override
+ public void discoverService(String type, INsdDiscoverServiceCallback callback, int listenerId) {
+ mHandler.post(() -> discoverServiceInternal(type, callback, listenerId));
+ }
+
+ private void discoverServiceInternal(
+ String type, INsdDiscoverServiceCallback callback, int listenerId) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ "Discovering services."
+ + " Listener ID: "
+ + listenerId
+ + ", service type: "
+ + type);
+
+ DiscoveryListener listener = new DiscoveryListener(listenerId, type, callback);
+ mDiscoveryListeners.append(listenerId, listener);
+ DiscoveryRequest discoveryRequest =
+ new DiscoveryRequest.Builder(type).setNetwork(null).build();
+ mNsdManager.discoverServices(discoveryRequest, mExecutor, listener);
+ }
+
+ @Override
+ public void stopServiceDiscovery(int listenerId) {
+ mHandler.post(() -> stopServiceDiscoveryInternal(listenerId));
+ }
+
+ private void stopServiceDiscoveryInternal(int listenerId) {
+ checkOnHandlerThread();
+
+ DiscoveryListener listener = mDiscoveryListeners.get(listenerId);
+ if (listener == null) {
+ Log.w(
+ TAG,
+ "Failed to stop service discovery. Listener ID "
+ + listenerId
+ + ". The listener is null.");
+ return;
+ }
+
+ Log.i(TAG, "Stopping service discovery. Listener: " + listener);
+ mNsdManager.stopServiceDiscovery(listener);
+ }
+
+ @Override
+ public void resolveService(
+ String name, String type, INsdResolveServiceCallback callback, int listenerId) {
+ mHandler.post(() -> resolveServiceInternal(name, type, callback, listenerId));
+ }
+
+ private void resolveServiceInternal(
+ String name, String type, INsdResolveServiceCallback callback, int listenerId) {
+ checkOnHandlerThread();
+
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName(name);
+ serviceInfo.setServiceType(type);
+ serviceInfo.setNetwork(null);
+ Log.i(
+ TAG,
+ "Resolving service."
+ + " Listener ID: "
+ + listenerId
+ + ", service name: "
+ + name
+ + ", service type: "
+ + type);
+
+ ServiceInfoListener listener = new ServiceInfoListener(serviceInfo, listenerId, callback);
+ mServiceInfoListeners.append(listenerId, listener);
+ mNsdManager.registerServiceInfoCallback(serviceInfo, mExecutor, listener);
+ }
+
+ @Override
+ public void stopServiceResolution(int listenerId) {
+ mHandler.post(() -> stopServiceResolutionInternal(listenerId));
+ }
+
+ private void stopServiceResolutionInternal(int listenerId) {
+ checkOnHandlerThread();
+
+ ServiceInfoListener listener = mServiceInfoListeners.get(listenerId);
+ if (listener == null) {
+ Log.w(
+ TAG,
+ "Failed to stop service resolution. Listener ID: "
+ + listenerId
+ + ". The listener is null.");
+ return;
+ }
+
+ Log.i(TAG, "Stopping service resolution. Listener: " + listener);
+
+ try {
+ mNsdManager.unregisterServiceInfoCallback(listener);
+ } catch (IllegalArgumentException e) {
+ Log.w(
+ TAG,
+ "Failed to stop the service resolution because it's already stopped. Listener: "
+ + listener);
+ }
+ }
+
private void checkOnHandlerThread() {
if (mHandler.getLooper().getThread() != Thread.currentThread()) {
throw new IllegalStateException(
@@ -368,4 +480,166 @@
popAndRunNext();
}
}
+
+ private final class DiscoveryListener implements NsdManager.DiscoveryListener {
+ private final int mListenerId;
+ private final String mType;
+ private final INsdDiscoverServiceCallback mDiscoverServiceCallback;
+
+ DiscoveryListener(
+ int listenerId,
+ @NonNull String type,
+ @NonNull INsdDiscoverServiceCallback discoverServiceCallback) {
+ mListenerId = listenerId;
+ mType = type;
+ mDiscoverServiceCallback = discoverServiceCallback;
+ }
+
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ Log.e(
+ TAG,
+ "Failed to start service discovery."
+ + " Error code: "
+ + errorCode
+ + ", listener: "
+ + this);
+ mDiscoveryListeners.remove(mListenerId);
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ Log.e(
+ TAG,
+ "Failed to stop service discovery."
+ + " Error code: "
+ + errorCode
+ + ", listener: "
+ + this);
+ mDiscoveryListeners.remove(mListenerId);
+ }
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ Log.i(TAG, "Started service discovery. Listener: " + this);
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ Log.i(TAG, "Stopped service discovery. Listener: " + this);
+ mDiscoveryListeners.remove(mListenerId);
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ Log.i(TAG, "Found service: " + serviceInfo);
+ try {
+ mDiscoverServiceCallback.onServiceDiscovered(
+ serviceInfo.getServiceName(), mType, true);
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ Log.i(TAG, "Lost service: " + serviceInfo);
+ try {
+ mDiscoverServiceCallback.onServiceDiscovered(
+ serviceInfo.getServiceName(), mType, false);
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ID: " + mListenerId + ", type: " + mType;
+ }
+ }
+
+ private final class ServiceInfoListener implements NsdManager.ServiceInfoCallback {
+ private final String mName;
+ private final String mType;
+ private final INsdResolveServiceCallback mResolveServiceCallback;
+ private final int mListenerId;
+
+ ServiceInfoListener(
+ @NonNull NsdServiceInfo serviceInfo,
+ int listenerId,
+ @NonNull INsdResolveServiceCallback resolveServiceCallback) {
+ mName = serviceInfo.getServiceName();
+ mType = serviceInfo.getServiceType();
+ mListenerId = listenerId;
+ mResolveServiceCallback = resolveServiceCallback;
+ }
+
+ @Override
+ public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
+ Log.e(
+ TAG,
+ "Failed to register service info callback."
+ + " Listener ID: "
+ + mListenerId
+ + ", error: "
+ + errorCode
+ + ", service name: "
+ + mName
+ + ", service type: "
+ + mType);
+ }
+
+ @Override
+ public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+ Log.i(
+ TAG,
+ "Service is resolved. "
+ + " Listener ID: "
+ + mListenerId
+ + ", serviceInfo: "
+ + serviceInfo);
+ List<String> addresses = new ArrayList<>();
+ for (InetAddress address : serviceInfo.getHostAddresses()) {
+ if (address instanceof Inet6Address) {
+ addresses.add(address.getHostAddress());
+ }
+ }
+ List<DnsTxtAttribute> txtList = new ArrayList<>();
+ for (Map.Entry<String, byte[]> entry : serviceInfo.getAttributes().entrySet()) {
+ DnsTxtAttribute attribute = new DnsTxtAttribute();
+ attribute.name = entry.getKey();
+ attribute.value = Arrays.copyOf(entry.getValue(), entry.getValue().length);
+ txtList.add(attribute);
+ }
+ // TODO: b/329018320 - Use the serviceInfo.getExpirationTime to derive TTL.
+ int ttlSeconds = 10;
+ try {
+ mResolveServiceCallback.onServiceResolved(
+ serviceInfo.getHostname(),
+ serviceInfo.getServiceName(),
+ serviceInfo.getServiceType(),
+ serviceInfo.getPort(),
+ addresses,
+ txtList,
+ ttlSeconds);
+
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public void onServiceLost() {}
+
+ @Override
+ public void onServiceInfoCallbackUnregistered() {
+ Log.i(TAG, "The service info callback is unregistered. Listener: " + this);
+ mServiceInfoListeners.remove(mListenerId);
+ }
+
+ @Override
+ public String toString() {
+ return "ID: " + mListenerId + ", service name: " + mName + ", service type: " + mType;
+ }
+ }
}
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index 39a1671..491331c 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -17,6 +17,7 @@
package android.net.thread;
import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
@@ -37,6 +38,7 @@
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.TapTestNetworkTracker;
import android.net.thread.utils.ThreadFeatureCheckerRule;
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
@@ -65,6 +67,7 @@
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
/** Integration test cases for Service Discovery feature. */
@@ -96,15 +99,15 @@
private final Context mContext = ApplicationProvider.getApplicationContext();
private final ThreadNetworkControllerWrapper mController =
ThreadNetworkControllerWrapper.newInstance(mContext);
-
+ private final OtDaemonController mOtCtl = new OtDaemonController();
private HandlerThread mHandlerThread;
private NsdManager mNsdManager;
private TapTestNetworkTracker mTestNetworkTracker;
private List<FullThreadDevice> mFtds;
+ private List<RegistrationListener> mRegistrationListeners = new ArrayList<>();
@Before
public void setUp() throws Exception {
-
mController.joinAndWait(DEFAULT_DATASET);
mNsdManager = mContext.getSystemService(NsdManager.class);
@@ -127,6 +130,9 @@
@After
public void tearDown() throws Exception {
+ for (RegistrationListener listener : mRegistrationListeners) {
+ unregisterService(listener);
+ }
for (FullThreadDevice ftd : mFtds) {
// Clear registered SRP hosts and services
if (ftd.isSrpHostRegistered()) {
@@ -314,6 +320,176 @@
assertThat(txtMap.get("mn")).isEqualTo("Thread Border Router".getBytes(UTF_8));
}
+ @Test
+ public void discoveryProxy_multipleClientsBrowseAndResolveServiceOverMdns() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ RegistrationListener listener = new RegistrationListener();
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceType("_testservice._tcp");
+ info.setServiceName("test-service");
+ info.setPort(12345);
+ info.setHostname("testhost");
+ info.setHostAddresses(List.of(parseNumericAddress("2001::1")));
+ info.setAttribute("key1", bytes(0x01, 0x02));
+ info.setAttribute("key2", bytes(0x03));
+ registerService(info, listener);
+ mRegistrationListeners.add(listener);
+ for (int i = 0; i < NUM_FTD; ++i) {
+ FullThreadDevice ftd = mFtds.get(i);
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ ftd.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
+ }
+ final ArrayList<NsdServiceInfo> browsedServices = new ArrayList<>();
+ final ArrayList<NsdServiceInfo> resolvedServices = new ArrayList<>();
+ final ArrayList<Thread> threads = new ArrayList<>();
+ for (int i = 0; i < NUM_FTD; ++i) {
+ browsedServices.add(null);
+ resolvedServices.add(null);
+ }
+ for (int i = 0; i < NUM_FTD; ++i) {
+ final FullThreadDevice ftd = mFtds.get(i);
+ final int index = i;
+ Runnable task =
+ () -> {
+ browsedServices.set(
+ index,
+ ftd.browseService("_testservice._tcp.default.service.arpa."));
+ resolvedServices.set(
+ index,
+ ftd.resolveService(
+ "test-service", "_testservice._tcp.default.service.arpa."));
+ };
+ threads.add(new Thread(task));
+ }
+ for (Thread thread : threads) {
+ thread.start();
+ }
+ for (Thread thread : threads) {
+ thread.join();
+ }
+
+ for (int i = 0; i < NUM_FTD; ++i) {
+ NsdServiceInfo browsedService = browsedServices.get(i);
+ assertThat(browsedService.getServiceName()).isEqualTo("test-service");
+ assertThat(browsedService.getPort()).isEqualTo(12345);
+
+ NsdServiceInfo resolvedService = resolvedServices.get(i);
+ assertThat(resolvedService.getServiceName()).isEqualTo("test-service");
+ assertThat(resolvedService.getPort()).isEqualTo(12345);
+ assertThat(resolvedService.getHostname()).isEqualTo("testhost.default.service.arpa.");
+ assertThat(resolvedService.getHostAddresses())
+ .containsExactly(parseNumericAddress("2001::1"));
+ assertThat(resolvedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+ }
+ }
+
+ @Test
+ public void discoveryProxy_browseAndResolveServiceAtSrpServer() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------+------ SRP client
+ * (Cuttlefish) |
+ * +------ DNS client
+ *
+ * </pre>
+ */
+ FullThreadDevice srpClient = mFtds.get(0);
+ srpClient.joinNetwork(DEFAULT_DATASET);
+ srpClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ srpClient.setSrpHostname("my-host");
+ srpClient.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001::1")));
+ srpClient.addSrpService(
+ "my-service",
+ "_test._udp",
+ List.of("_sub1"),
+ 12345 /* port */,
+ Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+
+ FullThreadDevice dnsClient = mFtds.get(1);
+ dnsClient.joinNetwork(DEFAULT_DATASET);
+ dnsClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ dnsClient.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
+
+ NsdServiceInfo browsedService = dnsClient.browseService("_test._udp.default.service.arpa.");
+ assertThat(browsedService.getServiceName()).isEqualTo("my-service");
+ assertThat(browsedService.getPort()).isEqualTo(12345);
+ assertThat(browsedService.getHostname()).isEqualTo("my-host.default.service.arpa.");
+ assertThat(browsedService.getHostAddresses())
+ .containsExactly(parseNumericAddress("2001::1"));
+ assertThat(browsedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+
+ NsdServiceInfo resolvedService =
+ dnsClient.resolveService("my-service", "_test._udp.default.service.arpa.");
+ assertThat(resolvedService.getServiceName()).isEqualTo("my-service");
+ assertThat(resolvedService.getPort()).isEqualTo(12345);
+ assertThat(resolvedService.getHostname()).isEqualTo("my-host.default.service.arpa.");
+ assertThat(resolvedService.getHostAddresses())
+ .containsExactly(parseNumericAddress("2001::1"));
+ assertThat(resolvedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+ }
+
+ private void registerService(NsdServiceInfo serviceInfo, RegistrationListener listener)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, listener);
+ listener.waitForRegistered();
+ }
+
+ private void unregisterService(RegistrationListener listener)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mNsdManager.unregisterService(listener);
+ listener.waitForUnregistered();
+ }
+
+ private static class RegistrationListener implements NsdManager.RegistrationListener {
+ private final CompletableFuture<Void> mRegisteredFuture = new CompletableFuture<>();
+ private final CompletableFuture<Void> mUnRegisteredFuture = new CompletableFuture<>();
+
+ RegistrationListener() {}
+
+ @Override
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {}
+
+ @Override
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {}
+
+ @Override
+ public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+ mRegisteredFuture.complete(null);
+ }
+
+ @Override
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+ mUnRegisteredFuture.complete(null);
+ }
+
+ public void waitForRegistered()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mRegisteredFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ }
+
+ public void waitForUnregistered()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mUnRegisteredFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ }
+ }
+
private static byte[] bytes(int... byteInts) {
byte[] bytes = new byte[byteInts.length];
for (int i = 0; i < byteInts.length; ++i) {
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 600b662..f7bb9ff 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -24,6 +24,7 @@
import android.net.InetAddresses;
import android.net.IpPrefix;
+import android.net.nsd.NsdServiceInfo;
import android.net.thread.ActiveOperationalDataset;
import com.google.errorprone.annotations.FormatMethod;
@@ -34,6 +35,7 @@
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Inet6Address;
+import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
@@ -327,6 +329,55 @@
return false;
}
+ /** Sets the DNS server address. */
+ public void setDnsServerAddress(String address) {
+ executeCommand("dns config " + address);
+ }
+
+ /** Returns the first browsed service instance of {@code serviceType}. */
+ public NsdServiceInfo browseService(String serviceType) {
+ // CLI output:
+ // DNS browse response for _testservice._tcp.default.service.arpa.
+ // test-service
+ // Port:12345, Priority:0, Weight:0, TTL:10
+ // Host:testhost.default.service.arpa.
+ // HostAddress:2001:0:0:0:0:0:0:1 TTL:10
+ // TXT:[key1=0102, key2=03] TTL:10
+
+ List<String> lines = executeCommand("dns browse " + serviceType);
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceName(lines.get(1));
+ info.setServiceType(serviceType);
+ info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(2)));
+ info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(3)));
+ info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(4))));
+ DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(5), info);
+
+ return info;
+ }
+
+ /** Returns the resolved service instance. */
+ public NsdServiceInfo resolveService(String serviceName, String serviceType) {
+ // CLI output:
+ // DNS service resolution response for test-service for service
+ // _test._tcp.default.service.arpa.
+ // Port:12345, Priority:0, Weight:0, TTL:10
+ // Host:Android.default.service.arpa.
+ // HostAddress:2001:0:0:0:0:0:0:1 TTL:10
+ // TXT:[key1=0102, key2=03] TTL:10
+
+ List<String> lines = executeCommand("dns service %s %s", serviceName, serviceType);
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceName(serviceName);
+ info.setServiceType(serviceType);
+ info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(1)));
+ info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(2)));
+ info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(3))));
+ DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(4), info);
+
+ return info;
+ }
+
/** Runs the "factoryreset" command on the device. */
public void factoryReset() {
try {
@@ -454,4 +505,45 @@
private static String toHexString(byte[] bytes) {
return base16().encode(bytes);
}
+
+ private static final class DnsServiceCliOutputParser {
+ /** Returns the first match in the input of a given regex pattern. */
+ private static Matcher firstMatchOf(String input, String regex) {
+ Matcher matcher = Pattern.compile(regex).matcher(input);
+ matcher.find();
+ return matcher;
+ }
+
+ // Example: "Port:12345"
+ private static int parsePort(String line) {
+ return Integer.parseInt(firstMatchOf(line, "Port:(\\d+)").group(1));
+ }
+
+ // Example: "Host:Android.default.service.arpa."
+ private static String parseHostname(String line) {
+ return firstMatchOf(line, "Host:(.+)").group(1);
+ }
+
+ // Example: "HostAddress:2001:0:0:0:0:0:0:1"
+ private static InetAddress parseHostAddress(String line) {
+ return InetAddresses.parseNumericAddress(
+ firstMatchOf(line, "HostAddress:([^ ]+)").group(1));
+ }
+
+ // Example: "TXT:[key1=0102, key2=03]"
+ private static void parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo) {
+ String txtString = firstMatchOf(line, "TXT:\\[([^\\]]+)\\]").group(1);
+ for (String txtEntry : txtString.split(",")) {
+ String[] nameAndValue = txtEntry.trim().split("=");
+ String name = nameAndValue[0];
+ String value = nameAndValue[1];
+ byte[] bytes = new byte[value.length() / 2];
+ for (int i = 0; i < value.length(); i += 2) {
+ byte b = (byte) ((value.charAt(i) - '0') << 4 | (value.charAt(i + 1) - '0'));
+ bytes[i / 2] = b;
+ }
+ serviceInfo.setAttribute(name, bytes);
+ }
+ }
+ }
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index 7c7f9d6..ade0669 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -68,6 +68,12 @@
return output.contains("up");
}
+ /** Returns the ML-EID of the device. */
+ public Inet6Address getMlEid() {
+ String addressStr = executeCommand("ipaddr mleid").split("\n")[0].trim();
+ return (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
+ }
+
public String executeCommand(String cmd) {
return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
}
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index d860166..8886c73 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -23,6 +23,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.spy;
@@ -30,24 +31,30 @@
import static org.mockito.Mockito.verify;
import android.net.InetAddresses;
+import android.net.nsd.DiscoveryRequest;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
import android.os.test.TestLooper;
import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
+import com.android.server.thread.openthread.INsdResolveServiceCallback;
import com.android.server.thread.openthread.INsdStatusReceiver;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.net.InetAddress;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -57,6 +64,8 @@
@Mock private INsdStatusReceiver mRegistrationReceiver;
@Mock private INsdStatusReceiver mUnregistrationReceiver;
+ @Mock private INsdDiscoverServiceCallback mDiscoverServiceCallback;
+ @Mock private INsdResolveServiceCallback mResolveServiceCallback;
private TestLooper mTestLooper;
private NsdPublisher mNsdPublisher;
@@ -469,6 +478,165 @@
}
@Test
+ public void discoverService_serviceDiscovered() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+ verify(mMockNsdManager, times(1))
+ .discoverServices(
+ eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+ any(Executor.class),
+ discoveryListenerArgumentCaptor.capture());
+ NsdManager.DiscoveryListener actualDiscoveryListener =
+ discoveryListenerArgumentCaptor.getValue();
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType(null);
+ actualDiscoveryListener.onServiceFound(serviceInfo);
+ mTestLooper.dispatchAll();
+
+ verify(mDiscoverServiceCallback, times(1))
+ .onServiceDiscovered("test", "_test._tcp", true /* isFound */);
+ }
+
+ @Test
+ public void discoverService_serviceLost() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+ verify(mMockNsdManager, times(1))
+ .discoverServices(
+ eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+ any(Executor.class),
+ discoveryListenerArgumentCaptor.capture());
+ NsdManager.DiscoveryListener actualDiscoveryListener =
+ discoveryListenerArgumentCaptor.getValue();
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType(null);
+ actualDiscoveryListener.onServiceLost(serviceInfo);
+ mTestLooper.dispatchAll();
+
+ verify(mDiscoverServiceCallback, times(1))
+ .onServiceDiscovered("test", "_test._tcp", false /* isFound */);
+ }
+
+ @Test
+ public void stopServiceDiscovery() {
+ prepareTest();
+
+ mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+ verify(mMockNsdManager, times(1))
+ .discoverServices(
+ eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+ any(Executor.class),
+ discoveryListenerArgumentCaptor.capture());
+ NsdManager.DiscoveryListener actualDiscoveryListener =
+ discoveryListenerArgumentCaptor.getValue();
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType(null);
+ actualDiscoveryListener.onServiceFound(serviceInfo);
+ mNsdPublisher.stopServiceDiscovery(10 /* listenerId */);
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(1)).stopServiceDiscovery(actualDiscoveryListener);
+ }
+
+ @Test
+ public void resolveService_serviceResolved() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.resolveService(
+ "test", "_test._tcp", mResolveServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdServiceInfo> serviceInfoArgumentCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.ServiceInfoCallback> serviceInfoCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.ServiceInfoCallback.class);
+ verify(mMockNsdManager, times(1))
+ .registerServiceInfoCallback(
+ serviceInfoArgumentCaptor.capture(),
+ any(Executor.class),
+ serviceInfoCallbackArgumentCaptor.capture());
+ assertThat(serviceInfoArgumentCaptor.getValue().getServiceName()).isEqualTo("test");
+ assertThat(serviceInfoArgumentCaptor.getValue().getServiceType()).isEqualTo("_test._tcp");
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType("_test._tcp");
+ serviceInfo.setPort(12345);
+ serviceInfo.setHostname("test-host");
+ serviceInfo.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("2001::1"),
+ InetAddress.parseNumericAddress("2001::2")));
+ serviceInfo.setAttribute("key1", new byte[] {(byte) 0x01, (byte) 0x02});
+ serviceInfo.setAttribute("key2", new byte[] {(byte) 0x03});
+ serviceInfoCallbackArgumentCaptor.getValue().onServiceUpdated(serviceInfo);
+ mTestLooper.dispatchAll();
+
+ verify(mResolveServiceCallback, times(1))
+ .onServiceResolved(
+ eq("test-host"),
+ eq("test"),
+ eq("_test._tcp"),
+ eq(12345),
+ eq(List.of("2001::1", "2001::2")),
+ argThat(
+ new TxtMatcher(
+ List.of(
+ makeTxtAttribute("key1", List.of(0x01, 0x02)),
+ makeTxtAttribute("key2", List.of(0x03))))),
+ anyInt());
+ }
+
+ @Test
+ public void stopServiceResolution() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.resolveService(
+ "test", "_test._tcp", mResolveServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdServiceInfo> serviceInfoArgumentCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.ServiceInfoCallback> serviceInfoCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.ServiceInfoCallback.class);
+ verify(mMockNsdManager, times(1))
+ .registerServiceInfoCallback(
+ serviceInfoArgumentCaptor.capture(),
+ any(Executor.class),
+ serviceInfoCallbackArgumentCaptor.capture());
+ assertThat(serviceInfoArgumentCaptor.getValue().getServiceName()).isEqualTo("test");
+ assertThat(serviceInfoArgumentCaptor.getValue().getServiceType()).isEqualTo("_test._tcp");
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType("_test._tcp");
+ serviceInfo.setPort(12345);
+ serviceInfo.setHostname("test-host");
+ serviceInfo.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("2001::1"),
+ InetAddress.parseNumericAddress("2001::2")));
+ serviceInfo.setAttribute("key1", new byte[] {(byte) 0x01, (byte) 0x02});
+ serviceInfo.setAttribute("key2", new byte[] {(byte) 0x03});
+ serviceInfoCallbackArgumentCaptor.getValue().onServiceUpdated(serviceInfo);
+ mNsdPublisher.stopServiceResolution(10 /* listenerId */);
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(1))
+ .unregisterServiceInfoCallback(serviceInfoCallbackArgumentCaptor.getValue());
+ }
+
+ @Test
public void reset_unregisterAll() {
prepareTest();
@@ -582,6 +750,30 @@
return addresses;
}
+ private static class TxtMatcher implements ArgumentMatcher<List<DnsTxtAttribute>> {
+ private final List<DnsTxtAttribute> mAttributes;
+
+ TxtMatcher(List<DnsTxtAttribute> attributes) {
+ mAttributes = attributes;
+ }
+
+ @Override
+ public boolean matches(List<DnsTxtAttribute> argument) {
+ if (argument.size() != mAttributes.size()) {
+ return false;
+ }
+ for (int i = 0; i < argument.size(); ++i) {
+ if (!Objects.equals(argument.get(i).name, mAttributes.get(i).name)) {
+ return false;
+ }
+ if (!Arrays.equals(argument.get(i).value, mAttributes.get(i).value)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
// @Before and @Test run in different threads. NsdPublisher requires the jobs are run on the
// thread looper, so TestLooper needs to be created inside each test case to install the
// correct looper.