Merge "[mdns] support regsitering hosts in NsdPublisher" into main

This commit is contained in:
Handa Wang 2024-03-05 12:15:17 +00:00 • committed by Gerrit Code Review
commit b7347bd441
6 changed files with 848 additions and 2 deletions

View file

@ -20,6 +20,7 @@ import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
import android.annotation.NonNull;
import android.content.Context;
import android.net.InetAddresses;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
@ -33,6 +34,7 @@ import com.android.server.thread.openthread.DnsTxtAttribute;
import com.android.server.thread.openthread.INsdPublisher;
import com.android.server.thread.openthread.INsdStatusReceiver;
import java.net.InetAddress;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
@ -119,6 +121,30 @@ public final class NsdPublisher extends INsdPublisher.Stub {
return serviceInfo;
}
@Override
public void registerHost(
String name, List<String> addresses, INsdStatusReceiver receiver, int listenerId) {
postRegistrationJob(
() -> {
NsdServiceInfo serviceInfo = buildServiceInfoForHost(name, addresses);
registerInternal(serviceInfo, receiver, listenerId, "host");
});
}
private static NsdServiceInfo buildServiceInfoForHost(
String name, List<String> addressStrings) {
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setHostname(name);
ArrayList<InetAddress> addresses = new ArrayList<>(addressStrings.size());
for (String addressString : addressStrings) {
addresses.add(InetAddresses.parseNumericAddress(addressString));
}
serviceInfo.setHostAddresses(addresses);
return serviceInfo;
}
private void registerInternal(
NsdServiceInfo serviceInfo,
INsdStatusReceiver receiver,

View file

@ -31,6 +31,7 @@ java_defaults {
"net-utils-device-common",
"net-utils-device-common-bpf",
"testables",
"ThreadNetworkTestUtils",
"truth",
],
libs: [

View file

@ -0,0 +1,351 @@
/*
* Copyright (C) 2024 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.thread;
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
import static android.net.thread.utils.IntegrationTestUtils.discoverService;
import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
import static android.net.thread.utils.IntegrationTestUtils.resolveService;
import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
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.os.HandlerThread;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.google.common.truth.Correspondence;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.net.Inet6Address;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
/** Integration test cases for Service Discovery feature. */
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ServiceDiscoveryTest {
private static final String TAG = ServiceDiscoveryTest.class.getSimpleName();
private static final int NUM_FTD = 3;
private final Context mContext = ApplicationProvider.getApplicationContext();
private HandlerThread mHandlerThread;
private ThreadNetworkController mController;
private NsdManager mNsdManager;
private TapTestNetworkTracker mTestNetworkTracker;
private List<FullThreadDevice> mFtds;
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
private static final Correspondence<byte[], byte[]> BYTE_ARRAY_EQUALITY =
Correspondence.from(Arrays::equals, "is equivalent to");
@Before
public void setUp() throws Exception {
final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
if (manager != null) {
mController = manager.getAllThreadNetworkControllers().get(0);
}
// Run the tests on only devices where the Thread feature is available.
assumeNotNull(mController);
// Run the tests only when the device uses simulated Thread radio.
assumeTrue(isSimulatedThreadRadioSupported());
// BR forms a network.
CompletableFuture<Void> joinFuture = new CompletableFuture<>();
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
mNsdManager = mContext.getSystemService(NsdManager.class);
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
assertThat(mTestNetworkTracker).isNotNull();
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
NETWORK_SETTINGS,
() -> {
CompletableFuture<Void> future = new CompletableFuture<>();
mController.setTestNetworkAsUpstream(
mTestNetworkTracker.getInterfaceName(),
directExecutor(),
v -> future.complete(null));
future.get(5, SECONDS);
});
// Create the FTDs in setUp() so that the FTDs can be safely released in tearDown().
// Don't create new FTDs in test cases.
mFtds = new ArrayList<>();
for (int i = 0; i < NUM_FTD; ++i) {
FullThreadDevice ftd = new FullThreadDevice(10 + i /* node ID */);
ftd.autoStartSrpClient();
mFtds.add(ftd);
}
}
@After
public void tearDown() throws Exception {
if (mController == null) {
return;
}
if (!isSimulatedThreadRadioSupported()) {
return;
}
for (FullThreadDevice ftd : mFtds) {
// Clear registered SRP hosts and services
if (ftd.isSrpHostRegistered()) {
ftd.removeSrpHost();
}
ftd.destroy();
}
if (mTestNetworkTracker != null) {
mTestNetworkTracker.tearDown();
}
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
mHandlerThread.join();
}
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
NETWORK_SETTINGS,
() -> {
CompletableFuture<Void> setUpstreamFuture = new CompletableFuture<>();
CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
mController.setTestNetworkAsUpstream(
null, directExecutor(), v -> setUpstreamFuture.complete(null));
mController.leave(directExecutor(), v -> leaveFuture.complete(null));
setUpstreamFuture.get(5, SECONDS);
leaveFuture.get(5, SECONDS);
});
}
@Test
public void advertisingProxy_multipleSrpClientsRegisterServices_servicesResolvableByMdns()
throws Exception {
/*
* <pre>
* Topology:
* Thread
* Border Router -------------- Full Thread device 1
* (Cuttlefish) |
* +------ Full Thread device 2
* |
* +------ Full Thread device 3
* </pre>
*/
// Creates Full Thread Devices (FTD) and let them join the network.
for (FullThreadDevice ftd : mFtds) {
ftd.joinNetwork(DEFAULT_DATASET);
ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
}
int randomId = new Random().nextInt(10_000);
String serviceNamePrefix = "service-" + randomId + "-";
String serviceTypePrefix = "_test" + randomId;
String hostnamePrefix = "host-" + randomId + "-";
// For every FTD, let it register an SRP service.
for (int i = 0; i < mFtds.size(); ++i) {
FullThreadDevice ftd = mFtds.get(i);
ftd.setSrpHostname(hostnamePrefix + i);
ftd.setSrpHostAddresses(List.of(ftd.getOmrAddress(), ftd.getMlEid()));
ftd.addSrpService(
serviceNamePrefix + i,
serviceTypePrefix + i + "._tcp",
List.of("_sub1", "_sub2"),
12345 /* port */,
Map.of("key1", bytes(0x01, 0x02), "key2", bytes(i)));
}
// Check the advertised services are discoverable and resolvable by NsdManager
for (int i = 0; i < mFtds.size(); ++i) {
NsdServiceInfo discoveredService =
discoverService(mNsdManager, serviceTypePrefix + i + "._tcp");
assertThat(discoveredService).isNotNull();
NsdServiceInfo resolvedService = resolveService(mNsdManager, discoveredService);
assertThat(resolvedService.getServiceName()).isEqualTo(serviceNamePrefix + i);
assertThat(resolvedService.getServiceType()).isEqualTo(serviceTypePrefix + i + "._tcp");
assertThat(resolvedService.getPort()).isEqualTo(12345);
assertThat(resolvedService.getAttributes())
.comparingValuesUsing(BYTE_ARRAY_EQUALITY)
.containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(i));
assertThat(resolvedService.getHostname()).isEqualTo(hostnamePrefix + i);
assertThat(resolvedService.getHostAddresses())
.containsExactly(mFtds.get(i).getOmrAddress());
}
}
@Test
public void advertisingProxy_srpClientUpdatesService_updatedServiceResolvableByMdns()
throws Exception {
/*
* <pre>
* Topology:
* Thread
* Border Router -------------- Full Thread device
* (Cuttlefish)
* </pre>
*/
// Creates a Full Thread Devices (FTD) and let it join the network.
FullThreadDevice ftd = mFtds.get(0);
ftd.joinNetwork(DEFAULT_DATASET);
ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
ftd.setSrpHostname("my-host");
ftd.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001:db8::1")));
ftd.addSrpService(
"my-service",
"_test._tcp",
Collections.emptyList() /* subtypes */,
12345 /* port */,
Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
// Update the host addresses
ftd.setSrpHostAddresses(
List.of(
(Inet6Address) parseNumericAddress("2001:db8::1"),
(Inet6Address) parseNumericAddress("2001:db8::2")));
// Update the service
ftd.updateSrpService(
"my-service", "_test._tcp", List.of("_sub3"), 11111, Map.of("key1", bytes(0x04)));
waitFor(ftd::isSrpHostRegistered, SERVICE_DISCOVERY_TIMEOUT);
// Check the advertised service is discoverable and resolvable by NsdManager
NsdServiceInfo discoveredService = discoverService(mNsdManager, "_test._tcp");
assertThat(discoveredService).isNotNull();
NsdServiceInfo resolvedService =
resolveServiceUntil(
mNsdManager,
discoveredService,
s -> s.getPort() == 11111 && s.getHostAddresses().size() == 2);
assertThat(resolvedService.getServiceName()).isEqualTo("my-service");
assertThat(resolvedService.getServiceType()).isEqualTo("_test._tcp");
assertThat(resolvedService.getPort()).isEqualTo(11111);
assertThat(resolvedService.getAttributes())
.comparingValuesUsing(BYTE_ARRAY_EQUALITY)
.containsExactly("key1", bytes(0x04));
assertThat(resolvedService.getHostname()).isEqualTo("my-host");
assertThat(resolvedService.getHostAddresses())
.containsExactly(
parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"));
}
@Test
public void advertisingProxy_srpClientUnregistersService_serviceIsNotDiscoverableByMdns()
throws Exception {
/*
* <pre>
* Topology:
* Thread
* Border Router -------------- Full Thread device
* (Cuttlefish)
* </pre>
*/
// Creates a Full Thread Devices (FTD) and let it join the network.
FullThreadDevice ftd = mFtds.get(0);
ftd.joinNetwork(DEFAULT_DATASET);
ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
ftd.setSrpHostname("my-host");
ftd.setSrpHostAddresses(
List.of(
(Inet6Address) parseNumericAddress("2001:db8::1"),
(Inet6Address) parseNumericAddress("2001:db8::2")));
ftd.addSrpService(
"my-service",
"_test._udp",
List.of("_sub1"),
12345 /* port */,
Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
// Wait for the service to be discoverable by NsdManager.
assertThat(discoverService(mNsdManager, "_test._udp")).isNotNull();
// Unregister the service.
CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
discoverForServiceLost(mNsdManager, "_test._udp", serviceLostFuture);
ftd.removeSrpService("my-service", "_test._udp", true /* notifyServer */);
// Verify the service becomes lost.
try {
serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
} finally {
mNsdManager.stopServiceDiscovery(listener);
}
assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_test._udp"));
}
private static byte[] bytes(int... byteInts) {
byte[] bytes = new byte[byteInts.length];
for (int i = 0; i < byteInts.length; ++i) {
bytes[i] = (byte) byteInts[i];
}
return bytes;
}
}

View file

@ -15,6 +15,7 @@
*/
package android.net.thread.utils;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
import static com.google.common.io.BaseEncoding.base16;
@ -25,15 +26,19 @@ import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.thread.ActiveOperationalDataset;
import com.google.errorprone.annotations.FormatMethod;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Inet6Address;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -191,7 +196,7 @@ public final class FullThreadDevice {
public void udpBind(Inet6Address address, int port) {
udpClose();
udpOpen();
executeCommand(String.format("udp bind %s %d", address.getHostAddress(), port));
executeCommand("udp bind %s %d", address.getHostAddress(), port);
}
/** Returns the message received on the UDP socket. */
@ -204,6 +209,117 @@ public final class FullThreadDevice {
return matcher.group(4);
}
/** Enables the SRP client and run in autostart mode. */
public void autoStartSrpClient() {
executeCommand("srp client autostart enable");
}
/** Sets the hostname (e.g. "MyHost") for the SRP client. */
public void setSrpHostname(String hostname) {
executeCommand("srp client host name " + hostname);
}
/** Sets the host addresses for the SRP client. */
public void setSrpHostAddresses(List<Inet6Address> addresses) {
executeCommand(
"srp client host address "
+ String.join(
" ",
addresses.stream().map(Inet6Address::getHostAddress).toList()));
}
/** Removes the SRP host */
public void removeSrpHost() {
executeCommand("srp client host remove 1 1");
}
/**
* Adds an SRP service for the SRP client and wait for the registration to complete.
*
* @param serviceName the service name like "MyService"
* @param serviceType the service type like "_test._tcp"
* @param subtypes the service subtypes like "_sub1"
* @param port the port number in range [1, 65535]
* @param txtMap the map of TXT names and values
* @throws TimeoutException if the service isn't registered within timeout
*/
public void addSrpService(
String serviceName,
String serviceType,
List<String> subtypes,
int port,
Map<String, byte[]> txtMap)
throws TimeoutException {
StringBuilder fullServiceType = new StringBuilder(serviceType);
for (String subtype : subtypes) {
fullServiceType.append(",").append(subtype);
}
executeCommand(
"srp client service add %s %s %d %d %d %s",
serviceName,
fullServiceType,
port,
0 /* priority */,
0 /* weight */,
txtMapToHexString(txtMap));
waitFor(() -> isSrpServiceRegistered(serviceName, serviceType), SERVICE_DISCOVERY_TIMEOUT);
}
/**
* Removes an SRP service for the SRP client.
*
* @param serviceName the service name like "MyService"
* @param serviceType the service type like "_test._tcp"
* @param notifyServer whether to notify SRP server about the removal
*/
public void removeSrpService(String serviceName, String serviceType, boolean notifyServer) {
String verb = notifyServer ? "remove" : "clear";
executeCommand("srp client service %s %s %s", verb, serviceName, serviceType);
}
/**
* Updates an existing SRP service for the SRP client.
*
* <p>This is essentially a 'remove' and an 'add' on the SRP client's side.
*
* @param serviceName the service name like "MyService"
* @param serviceType the service type like "_test._tcp"
* @param subtypes the service subtypes like "_sub1"
* @param port the port number in range [1, 65535]
* @param txtMap the map of TXT names and values
* @throws TimeoutException if the service isn't updated within timeout
*/
public void updateSrpService(
String serviceName,
String serviceType,
List<String> subtypes,
int port,
Map<String, byte[]> txtMap)
throws TimeoutException {
removeSrpService(serviceName, serviceType, false /* notifyServer */);
addSrpService(serviceName, serviceType, subtypes, port, txtMap);
}
/** Checks if an SRP service is registered. */
public boolean isSrpServiceRegistered(String serviceName, String serviceType) {
List<String> lines = executeCommand("srp client service");
for (String line : lines) {
if (line.contains(serviceName) && line.contains(serviceType)) {
return line.contains("Registered");
}
}
return false;
}
/** Checks if an SRP host is registered. */
public boolean isSrpHostRegistered() {
List<String> lines = executeCommand("srp client host");
for (String line : lines) {
return line.contains("Registered");
}
return false;
}
/** Runs the "factoryreset" command on the device. */
public void factoryReset() {
try {
@ -240,6 +356,11 @@ public final class FullThreadDevice {
ping(address, null, 100 /* size */, 1 /* count */);
}
@FormatMethod
private List<String> executeCommand(String commandFormat, Object... args) {
return executeCommand(String.format(commandFormat, args));
}
private List<String> executeCommand(String command) {
try {
mWriter.write(command + "\n");
@ -263,7 +384,7 @@ public final class FullThreadDevice {
if (line.equals("Done")) {
break;
}
if (line.startsWith("Error:")) {
if (line.startsWith("Error")) {
fail("ot-cli-ftd reported an error: " + line);
}
if (!line.startsWith("> ")) {
@ -272,4 +393,27 @@ public final class FullThreadDevice {
}
return result;
}
private static String txtMapToHexString(Map<String, byte[]> txtMap) {
if (txtMap == null) {
return "";
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, byte[]> entry : txtMap.entrySet()) {
int length = entry.getKey().length() + entry.getValue().length + 1;
sb.append(String.format("%02x", length));
sb.append(toHexString(entry.getKey()));
sb.append(toHexString("="));
sb.append(toHexString(entry.getValue()));
}
return sb.toString();
}
private static String toHexString(String s) {
return toHexString(s.getBytes(StandardCharsets.UTF_8));
}
private static String toHexString(byte[] bytes) {
return base16().encode(bytes);
}
}

View file

@ -23,12 +23,18 @@ import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_AD
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.net.TestNetworkInterface;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.net.thread.ThreadNetworkController;
import android.os.Handler;
import android.os.SystemClock;
import android.os.SystemProperties;
import androidx.annotation.NonNull;
import com.android.net.module.util.Struct;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.Ipv6Header;
@ -51,6 +57,7 @@ import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -66,6 +73,7 @@ public final class IntegrationTestUtils {
public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(30);
public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
private IntegrationTestUtils() {}
@ -289,4 +297,106 @@ public final class IntegrationTestUtils {
}
return false;
}
/** Return the first discovered service of {@code serviceType}. */
public static NsdServiceInfo discoverService(NsdManager nsdManager, String serviceType)
throws Exception {
CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
new DefaultDiscoveryListener() {
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {
serviceInfoFuture.complete(serviceInfo);
}
};
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
try {
serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
} finally {
nsdManager.stopServiceDiscovery(listener);
}
return serviceInfoFuture.get();
}
/**
* Returns the {@link NsdServiceInfo} when a service instance of {@code serviceType} gets lost.
*/
public static NsdManager.DiscoveryListener discoverForServiceLost(
NsdManager nsdManager,
String serviceType,
CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
NsdManager.DiscoveryListener listener =
new DefaultDiscoveryListener() {
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
serviceInfoFuture.complete(serviceInfo);
}
};
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
return listener;
}
/** Resolves the service. */
public static NsdServiceInfo resolveService(NsdManager nsdManager, NsdServiceInfo serviceInfo)
throws Exception {
return resolveServiceUntil(nsdManager, serviceInfo, s -> true);
}
/** Returns the first resolved service that satisfies the {@code predicate}. */
public static NsdServiceInfo resolveServiceUntil(
NsdManager nsdManager, NsdServiceInfo serviceInfo, Predicate<NsdServiceInfo> predicate)
throws Exception {
CompletableFuture<NsdServiceInfo> resolvedServiceInfoFuture = new CompletableFuture<>();
NsdManager.ServiceInfoCallback callback =
new DefaultServiceInfoCallback() {
@Override
public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
if (predicate.test(serviceInfo)) {
resolvedServiceInfoFuture.complete(serviceInfo);
}
}
};
nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback);
try {
return resolvedServiceInfoFuture.get(
SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
} finally {
nsdManager.unregisterServiceInfoCallback(callback);
}
}
private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
@Override
public void onDiscoveryStarted(String serviceType) {}
@Override
public void onDiscoveryStopped(String serviceType) {}
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {}
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {}
}
private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
@Override
public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
@Override
public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
@Override
public void onServiceLost() {}
@Override
public void onServiceInfoCallbackUnregistered() {}
}
}

View file

@ -28,6 +28,7 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.net.InetAddresses;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Handler;
@ -42,6 +43,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@ -281,6 +284,189 @@ public final class NsdPublisherTest {
verify(mUnregistrationReceiver, times(1)).onError(0);
}
@Test
public void registerHost_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
prepareTest();
mNsdPublisher.registerHost(
"MyHost",
List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(),
actualRegistrationListenerCaptor.capture());
NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
NsdManager.RegistrationListener actualRegistrationListener =
actualRegistrationListenerCaptor.getValue();
actualRegistrationListener.onServiceRegistered(actualServiceInfo);
mTestLooper.dispatchAll();
assertThat(actualServiceInfo.getServiceName()).isNull();
assertThat(actualServiceInfo.getServiceType()).isNull();
assertThat(actualServiceInfo.getSubtypes()).isEmpty();
assertThat(actualServiceInfo.getPort()).isEqualTo(0);
assertThat(actualServiceInfo.getAttributes()).isEmpty();
assertThat(actualServiceInfo.getHostname()).isEqualTo("MyHost");
assertThat(actualServiceInfo.getHostAddresses())
.isEqualTo(makeAddresses("2001:db8::1", "2001:db8::2", "2001:db8::3"));
verify(mRegistrationReceiver, times(1)).onSuccess();
}
@Test
public void registerHost_nsdManagerFails_serviceRegistrationFails() throws Exception {
prepareTest();
mNsdPublisher.registerHost(
"MyHost",
List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(),
actualRegistrationListenerCaptor.capture());
mTestLooper.dispatchAll();
NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
NsdManager.RegistrationListener actualRegistrationListener =
actualRegistrationListenerCaptor.getValue();
actualRegistrationListener.onRegistrationFailed(actualServiceInfo, FAILURE_INTERNAL_ERROR);
mTestLooper.dispatchAll();
assertThat(actualServiceInfo.getServiceName()).isNull();
assertThat(actualServiceInfo.getServiceType()).isNull();
assertThat(actualServiceInfo.getSubtypes()).isEmpty();
assertThat(actualServiceInfo.getPort()).isEqualTo(0);
assertThat(actualServiceInfo.getAttributes()).isEmpty();
assertThat(actualServiceInfo.getHostname()).isEqualTo("MyHost");
assertThat(actualServiceInfo.getHostAddresses())
.isEqualTo(makeAddresses("2001:db8::1", "2001:db8::2", "2001:db8::3"));
verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
}
@Test
public void registerHost_nsdManagerThrows_serviceRegistrationFails() throws Exception {
prepareTest();
doThrow(new IllegalArgumentException("NsdManager fails"))
.when(mMockNsdManager)
.registerService(any(), anyInt(), any(Executor.class), any());
mNsdPublisher.registerHost(
"MyHost",
List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
}
@Test
public void unregisterHost_nsdManagerSucceeds_serviceUnregistrationSucceeds() throws Exception {
prepareTest();
mNsdPublisher.registerHost(
"MyHost",
List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(Executor.class),
actualRegistrationListenerCaptor.capture());
NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
NsdManager.RegistrationListener actualRegistrationListener =
actualRegistrationListenerCaptor.getValue();
actualRegistrationListener.onServiceRegistered(actualServiceInfo);
mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
actualRegistrationListener.onServiceUnregistered(actualServiceInfo);
mTestLooper.dispatchAll();
verify(mUnregistrationReceiver, times(1)).onSuccess();
}
@Test
public void unregisterHost_nsdManagerFails_serviceUnregistrationFails() throws Exception {
prepareTest();
mNsdPublisher.registerHost(
"MyHost",
List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
mRegistrationReceiver,
16 /* listenerId */);
mTestLooper.dispatchAll();
ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
ArgumentCaptor.forClass(NsdServiceInfo.class);
ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
verify(mMockNsdManager, times(1))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(Executor.class),
actualRegistrationListenerCaptor.capture());
NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
NsdManager.RegistrationListener actualRegistrationListener =
actualRegistrationListenerCaptor.getValue();
actualRegistrationListener.onServiceRegistered(actualServiceInfo);
mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
actualRegistrationListener.onUnregistrationFailed(
actualServiceInfo, FAILURE_INTERNAL_ERROR);
mTestLooper.dispatchAll();
verify(mUnregistrationReceiver, times(1)).onError(0);
}
@Test
public void onOtDaemonDied_unregisterAll() {
prepareTest();
@ -336,11 +522,30 @@ public final class NsdPublisherTest {
actualRegistrationListenerCaptor.getAllValues().get(1);
actualListener2.onServiceRegistered(actualServiceInfoCaptor.getValue());
mNsdPublisher.registerHost(
"Myhost",
List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
mRegistrationReceiver,
18 /* listenerId */);
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(3))
.registerService(
actualServiceInfoCaptor.capture(),
eq(PROTOCOL_DNS_SD),
any(Executor.class),
actualRegistrationListenerCaptor.capture());
NsdManager.RegistrationListener actualListener3 =
actualRegistrationListenerCaptor.getAllValues().get(1);
actualListener3.onServiceRegistered(actualServiceInfoCaptor.getValue());
mNsdPublisher.onOtDaemonDied();
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
verify(mMockNsdManager, times(1)).unregisterService(actualListener2);
verify(mMockNsdManager, times(1)).unregisterService(actualListener3);
}
private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
@ -356,6 +561,15 @@ public final class NsdPublisherTest {
return txtAttribute;
}
private static List<InetAddress> makeAddresses(String... addressStrings) {
List<InetAddress> addresses = new ArrayList<>();
for (String addressString : addressStrings) {
addresses.add(InetAddresses.parseNumericAddress(addressString));
}
return addresses;
}
// @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.