[mdns] add API support for custom hostname
This commit adds support of regsitering a service with a custom
hostname. This is required to enable Advertising Proxy feature for Thread
devices.
For example:
```
NsdServiceInfo info = new NsdServiceInfo();
info.setServiceName("My Service");
info.setServiceType("_test._tcp");
info.setHostname("MyHost");
info.setHostAddresses(List.of(address1, address2));
nsdManager.registerService(info, PROTOCOL_DNS_SD, listener);
```
This CL also adds two new error codes for service/host registration
conflict: `FAILURE_SERVICE_NAME_CONFLICT` and
`FAILURE_HOST_NAME_CONFLICT`.
Bug: 284904661
Test: atest CtsNetTestCases FrameworksNetTests
Change-Id: I2fb974c38d851ba99144a18c7047084bbd61b938
This commit is contained in:
parent
ff474baf75
commit
1c2be6b391
5 changed files with 517 additions and 9 deletions
|
|
@ -159,6 +159,8 @@ public final class NsdManager {
|
|||
"com.android.net.flags.nsd_subtypes_support_enabled";
|
||||
static final String ADVERTISE_REQUEST_API =
|
||||
"com.android.net.flags.advertise_request_api";
|
||||
static final String NSD_CUSTOM_HOSTNAME_ENABLED =
|
||||
"com.android.net.flags.nsd_custom_hostname_enabled";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1237,7 +1239,7 @@ public final class NsdManager {
|
|||
*/
|
||||
public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
|
||||
@NonNull Executor executor, @NonNull RegistrationListener listener) {
|
||||
checkServiceInfo(serviceInfo);
|
||||
checkServiceInfoForRegistration(serviceInfo);
|
||||
checkProtocol(protocolType);
|
||||
final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo,
|
||||
protocolType);
|
||||
|
|
@ -1296,7 +1298,10 @@ public final class NsdManager {
|
|||
* @return Type and comma-separated list of subtypes, or null if invalid format.
|
||||
*/
|
||||
@Nullable
|
||||
private static Pair<String, String> getTypeAndSubtypes(@NonNull String typeWithSubtype) {
|
||||
private static Pair<String, String> getTypeAndSubtypes(@Nullable String typeWithSubtype) {
|
||||
if (typeWithSubtype == null) {
|
||||
return null;
|
||||
}
|
||||
final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype);
|
||||
if (!matcher.matches()) return null;
|
||||
// Reject specifications using leading subtypes with a dot
|
||||
|
|
@ -1327,10 +1332,7 @@ public final class NsdManager {
|
|||
@NonNull RegistrationListener listener) {
|
||||
final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
|
||||
final int protocolType = advertisingRequest.getProtocolType();
|
||||
if (serviceInfo.getPort() <= 0) {
|
||||
throw new IllegalArgumentException("Invalid port number");
|
||||
}
|
||||
checkServiceInfo(serviceInfo);
|
||||
checkServiceInfoForRegistration(serviceInfo);
|
||||
checkProtocol(protocolType);
|
||||
final int key;
|
||||
// For update only request, the old listener has to be reused
|
||||
|
|
@ -1607,7 +1609,7 @@ public final class NsdManager {
|
|||
@Deprecated
|
||||
public void resolveService(@NonNull NsdServiceInfo serviceInfo,
|
||||
@NonNull Executor executor, @NonNull ResolveListener listener) {
|
||||
checkServiceInfo(serviceInfo);
|
||||
checkServiceInfoForResolution(serviceInfo);
|
||||
int key = putListener(listener, executor, serviceInfo);
|
||||
try {
|
||||
mService.resolveService(key, serviceInfo);
|
||||
|
|
@ -1661,7 +1663,7 @@ public final class NsdManager {
|
|||
// TODO: use {@link DiscoveryRequest} to specify the service to be subscribed
|
||||
public void registerServiceInfoCallback(@NonNull NsdServiceInfo serviceInfo,
|
||||
@NonNull Executor executor, @NonNull ServiceInfoCallback listener) {
|
||||
checkServiceInfo(serviceInfo);
|
||||
checkServiceInfoForResolution(serviceInfo);
|
||||
int key = putListener(listener, executor, serviceInfo);
|
||||
try {
|
||||
mService.registerServiceInfoCallback(key, serviceInfo);
|
||||
|
|
@ -1706,7 +1708,7 @@ public final class NsdManager {
|
|||
}
|
||||
}
|
||||
|
||||
private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
|
||||
private static void checkServiceInfoForResolution(NsdServiceInfo serviceInfo) {
|
||||
Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
|
||||
if (TextUtils.isEmpty(serviceInfo.getServiceName())) {
|
||||
throw new IllegalArgumentException("Service name cannot be empty");
|
||||
|
|
@ -1715,4 +1717,46 @@ public final class NsdManager {
|
|||
throw new IllegalArgumentException("Service type cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the {@link NsdServiceInfo} is valid for registration.
|
||||
*
|
||||
* The following can be registered:
|
||||
* - A service with an optional host.
|
||||
* - A hostname with addresses.
|
||||
*
|
||||
* Note that:
|
||||
* - When registering a service, the service name, service type and port must be specified. If
|
||||
* hostname is specified, the host addresses can optionally be specified.
|
||||
* - When registering a host without a service, the addresses must be specified.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) {
|
||||
Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
|
||||
boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName());
|
||||
boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType());
|
||||
boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname());
|
||||
boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses());
|
||||
|
||||
if (serviceInfo.getPort() < 0) {
|
||||
throw new IllegalArgumentException("Invalid port");
|
||||
}
|
||||
|
||||
if (hasServiceType || hasServiceName || (serviceInfo.getPort() > 0)) {
|
||||
if (!(hasServiceType && hasServiceName && (serviceInfo.getPort() > 0))) {
|
||||
throw new IllegalArgumentException(
|
||||
"The service type, service name or port is missing");
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasServiceType && !hasHostname) {
|
||||
throw new IllegalArgumentException("No service or host specified in NsdServiceInfo");
|
||||
}
|
||||
|
||||
if (!hasServiceType && hasHostname && !hasHostAddresses) {
|
||||
// TODO: b/317946010 - This may be allowed when it supports registering KEY RR.
|
||||
throw new IllegalArgumentException("No host addresses specified in NsdServiceInfo");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,10 @@ public final class NsdServiceInfo implements Parcelable {
|
|||
|
||||
private static final String TAG = "NsdServiceInfo";
|
||||
|
||||
@Nullable
|
||||
private String mServiceName;
|
||||
|
||||
@Nullable
|
||||
private String mServiceType;
|
||||
|
||||
private final Set<String> mSubtypes;
|
||||
|
|
@ -59,6 +61,9 @@ public final class NsdServiceInfo implements Parcelable {
|
|||
|
||||
private final List<InetAddress> mHostAddresses;
|
||||
|
||||
@Nullable
|
||||
private String mHostname;
|
||||
|
||||
private int mPort;
|
||||
|
||||
@Nullable
|
||||
|
|
@ -90,6 +95,7 @@ public final class NsdServiceInfo implements Parcelable {
|
|||
mSubtypes = new ArraySet<>(other.getSubtypes());
|
||||
mTxtRecord = new ArrayMap<>(other.mTxtRecord);
|
||||
mHostAddresses = new ArrayList<>(other.getHostAddresses());
|
||||
mHostname = other.getHostname();
|
||||
mPort = other.getPort();
|
||||
mNetwork = other.getNetwork();
|
||||
mInterfaceIndex = other.getInterfaceIndex();
|
||||
|
|
@ -168,6 +174,43 @@ public final class NsdServiceInfo implements Parcelable {
|
|||
mHostAddresses.addAll(addresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hostname.
|
||||
*
|
||||
* <p>When a service is resolved, it returns the hostname of the resolved service . The top
|
||||
* level domain ".local." is omitted.
|
||||
*
|
||||
* <p>For example, it returns "MyHost" when the service's hostname is "MyHost.local.".
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
// @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
|
||||
@Nullable
|
||||
public String getHostname() {
|
||||
return mHostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom hostname for this service instance for registration.
|
||||
*
|
||||
* <p>A hostname must be in ".local." domain. The ".local." must be omitted when calling this
|
||||
* method.
|
||||
*
|
||||
* <p>For example, you should call setHostname("MyHost") to use the hostname "MyHost.local.".
|
||||
*
|
||||
* <p>If a hostname is set with this method, the addresses set with {@link #setHostAddresses}
|
||||
* will be registered with the hostname.
|
||||
*
|
||||
* <p>If the hostname is null (which is the default for a new {@link NsdServiceInfo}), a random
|
||||
* hostname is used and the addresses of this device will be registered.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
// @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
|
||||
public void setHostname(@Nullable String hostname) {
|
||||
mHostname = hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack txt information from a base-64 encoded byte array.
|
||||
*
|
||||
|
|
@ -454,6 +497,7 @@ public final class NsdServiceInfo implements Parcelable {
|
|||
.append(", type: ").append(mServiceType)
|
||||
.append(", subtypes: ").append(TextUtils.join(", ", mSubtypes))
|
||||
.append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
|
||||
.append(", hostname: ").append(mHostname)
|
||||
.append(", port: ").append(mPort)
|
||||
.append(", network: ").append(mNetwork);
|
||||
|
||||
|
|
@ -494,6 +538,7 @@ public final class NsdServiceInfo implements Parcelable {
|
|||
for (InetAddress address : mHostAddresses) {
|
||||
InetAddressUtils.parcelInetAddress(dest, address, flags);
|
||||
}
|
||||
dest.writeString(mHostname);
|
||||
}
|
||||
|
||||
/** Implement the Parcelable interface */
|
||||
|
|
@ -523,6 +568,7 @@ public final class NsdServiceInfo implements Parcelable {
|
|||
for (int i = 0; i < size; i++) {
|
||||
info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
|
||||
}
|
||||
info.mHostname = in.readString();
|
||||
return info;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ public class NsdServiceInfoTest {
|
|||
fullInfo.setSubtypes(Set.of("_thread", "_matter"));
|
||||
fullInfo.setPort(4242);
|
||||
fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
|
||||
fullInfo.setHostname("home");
|
||||
fullInfo.setNetwork(new Network(123));
|
||||
fullInfo.setInterfaceIndex(456);
|
||||
checkParcelable(fullInfo);
|
||||
|
|
@ -134,6 +135,7 @@ public class NsdServiceInfoTest {
|
|||
attributedInfo.setServiceType("_kitten._tcp");
|
||||
attributedInfo.setPort(4242);
|
||||
attributedInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
|
||||
attributedInfo.setHostname("home");
|
||||
attributedInfo.setAttribute("color", "pink");
|
||||
attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
|
||||
attributedInfo.setAttribute("adorable", (String) null);
|
||||
|
|
@ -169,6 +171,7 @@ public class NsdServiceInfoTest {
|
|||
assertEquals(original.getServiceName(), result.getServiceName());
|
||||
assertEquals(original.getServiceType(), result.getServiceType());
|
||||
assertEquals(original.getHost(), result.getHost());
|
||||
assertEquals(original.getHostname(), result.getHostname());
|
||||
assertTrue(original.getPort() == result.getPort());
|
||||
assertEquals(original.getNetwork(), result.getNetwork());
|
||||
assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ import org.junit.Before
|
|||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
private const val TAG = "NsdManagerTest"
|
||||
private const val TIMEOUT_MS = 2000L
|
||||
|
|
@ -162,7 +163,11 @@ class NsdManagerTest {
|
|||
private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
|
||||
private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
|
||||
private val serviceName2 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
|
||||
private val serviceName3 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
|
||||
private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
|
||||
private val serviceType2 = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
|
||||
private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
|
||||
private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
|
||||
private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
|
||||
private val ctsNetUtils by lazy{ CtsNetUtils(context) }
|
||||
|
||||
|
|
@ -1188,6 +1193,84 @@ class NsdManagerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRegisterServiceWithCustomHostAndAddresses_conflictDuringProbing_hostRenamed() {
|
||||
val si = makeTestServiceInfo(testNetwork1.network).apply {
|
||||
hostname = customHostname
|
||||
hostAddresses = listOf(
|
||||
parseNumericAddress("192.0.2.24"),
|
||||
parseNumericAddress("2001:db8::3"))
|
||||
}
|
||||
|
||||
val packetReader = TapPacketReader(Handler(handlerThread.looper),
|
||||
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
|
||||
packetReader.startAsyncForTest()
|
||||
handlerThread.waitForIdle(TIMEOUT_MS)
|
||||
|
||||
// Register service on testNetwork1
|
||||
val registrationRecord = NsdRegistrationRecord()
|
||||
nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
|
||||
registrationRecord)
|
||||
|
||||
tryTest {
|
||||
assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
|
||||
"Did not find a probe for the service")
|
||||
packetReader.sendResponse(buildConflictingAnnouncementForCustomHost())
|
||||
|
||||
// Registration must use an updated hostname to avoid the conflict
|
||||
val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
|
||||
// Service name is not renamed because there's no conflict on the service name.
|
||||
// TODO: b/283053491 - enable this check
|
||||
// assertEquals(serviceName, cb.serviceInfo.serviceName)
|
||||
val hostname = cb.serviceInfo.hostname ?: fail("Missing hostname")
|
||||
hostname.let {
|
||||
assertTrue("Unexpected registered hostname: $it",
|
||||
it.startsWith(customHostname) && it != customHostname)
|
||||
}
|
||||
} cleanupStep {
|
||||
nsdManager.unregisterService(registrationRecord)
|
||||
registrationRecord.expectCallback<ServiceUnregistered>()
|
||||
} cleanup {
|
||||
packetReader.handler.post { packetReader.stop() }
|
||||
handlerThread.waitForIdle(TIMEOUT_MS)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRegisterServiceWithCustomHostNoAddresses_noConflictDuringProbing_notRenamed() {
|
||||
val si = makeTestServiceInfo(testNetwork1.network).apply {
|
||||
hostname = customHostname
|
||||
}
|
||||
|
||||
val packetReader = TapPacketReader(Handler(handlerThread.looper),
|
||||
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
|
||||
packetReader.startAsyncForTest()
|
||||
handlerThread.waitForIdle(TIMEOUT_MS)
|
||||
|
||||
// Register service on testNetwork1
|
||||
val registrationRecord = NsdRegistrationRecord()
|
||||
nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
|
||||
registrationRecord)
|
||||
|
||||
tryTest {
|
||||
assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
|
||||
"Did not find a probe for the service")
|
||||
// Not a conflict because no record is registered for the hostname
|
||||
packetReader.sendResponse(buildConflictingAnnouncementForCustomHost())
|
||||
|
||||
// Registration is not renamed because there's no conflict
|
||||
val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
|
||||
assertEquals(serviceName, cb.serviceInfo.serviceName)
|
||||
assertEquals(customHostname, cb.serviceInfo.hostname)
|
||||
} cleanupStep {
|
||||
nsdManager.unregisterService(registrationRecord)
|
||||
registrationRecord.expectCallback<ServiceUnregistered>()
|
||||
} cleanup {
|
||||
packetReader.handler.post { packetReader.stop() }
|
||||
handlerThread.waitForIdle(TIMEOUT_MS)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRegisterWithConflictAfterProbing() {
|
||||
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
|
||||
|
|
@ -1263,6 +1346,52 @@ class NsdManagerTest {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: b/322282952 - Add the test case that the hostname is renamed due to a conflict after
|
||||
// probing succeeded.
|
||||
|
||||
@Test
|
||||
fun testRegisterServiceWithCustomHostNoAddresses_noConflictAfterProbing_notRenamed() {
|
||||
val si = makeTestServiceInfo(testNetwork1.network).apply {
|
||||
hostname = customHostname
|
||||
}
|
||||
|
||||
// Register service on testNetwork1
|
||||
val registrationRecord = NsdRegistrationRecord()
|
||||
val discoveryRecord = NsdDiscoveryRecord()
|
||||
val registeredService = registerService(registrationRecord, si)
|
||||
val packetReader = TapPacketReader(Handler(handlerThread.looper),
|
||||
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
|
||||
packetReader.startAsyncForTest()
|
||||
handlerThread.waitForIdle(TIMEOUT_MS)
|
||||
|
||||
tryTest {
|
||||
assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
|
||||
"No announcements sent after initial probing")
|
||||
|
||||
assertEquals(si.serviceName, registeredService.serviceName)
|
||||
assertEquals(si.hostname, registeredService.hostname)
|
||||
|
||||
// Send a conflicting announcement
|
||||
val conflictingAnnouncement = buildConflictingAnnouncementForCustomHost()
|
||||
packetReader.sendResponse(conflictingAnnouncement)
|
||||
|
||||
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
|
||||
testNetwork1.network, { it.run() }, discoveryRecord)
|
||||
|
||||
// The service is not renamed
|
||||
discoveryRecord.waitForServiceDiscovered(si.serviceName, serviceType)
|
||||
} cleanupStep {
|
||||
nsdManager.stopServiceDiscovery(discoveryRecord)
|
||||
discoveryRecord.expectCallback<DiscoveryStopped>()
|
||||
} cleanupStep {
|
||||
nsdManager.unregisterService(registrationRecord)
|
||||
registrationRecord.expectCallback<ServiceUnregistered>()
|
||||
} cleanup {
|
||||
packetReader.handler.post { packetReader.stop() }
|
||||
handlerThread.waitForIdle(TIMEOUT_MS)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that even if only a PTR record is received as a reply when discovering, without the
|
||||
// SRV, TXT, address records as recommended (but not mandated) by RFC 6763 12, the service can
|
||||
// still be discovered.
|
||||
|
|
@ -1447,6 +1576,212 @@ class NsdManagerTest {
|
|||
return Inet6Address.getByAddress(addrBytes) as Inet6Address
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAdvertisingAndDiscovery_servicesWithCustomHost_customHostAddressesFound() {
|
||||
val hostAddresses1 = listOf(
|
||||
parseNumericAddress("192.0.2.23"),
|
||||
parseNumericAddress("2001:db8::1"),
|
||||
parseNumericAddress("2001:db8::2"))
|
||||
val hostAddresses2 = listOf(
|
||||
parseNumericAddress("192.0.2.24"),
|
||||
parseNumericAddress("2001:db8::3"))
|
||||
val si1 = NsdServiceInfo().also {
|
||||
it.network = testNetwork1.network
|
||||
it.serviceName = serviceName
|
||||
it.serviceType = serviceType
|
||||
it.port = TEST_PORT
|
||||
it.hostname = customHostname
|
||||
it.hostAddresses = hostAddresses1
|
||||
}
|
||||
val si2 = NsdServiceInfo().also {
|
||||
it.network = testNetwork1.network
|
||||
it.serviceName = serviceName2
|
||||
it.serviceType = serviceType
|
||||
it.port = TEST_PORT + 1
|
||||
it.hostname = customHostname2
|
||||
it.hostAddresses = hostAddresses2
|
||||
}
|
||||
val registrationRecord1 = NsdRegistrationRecord()
|
||||
val registrationRecord2 = NsdRegistrationRecord()
|
||||
|
||||
val discoveryRecord1 = NsdDiscoveryRecord()
|
||||
val discoveryRecord2 = NsdDiscoveryRecord()
|
||||
tryTest {
|
||||
registerService(registrationRecord1, si1)
|
||||
|
||||
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
|
||||
testNetwork1.network, Executor { it.run() }, discoveryRecord1)
|
||||
|
||||
val discoveredInfo = discoveryRecord1.waitForServiceDiscovered(
|
||||
serviceName, serviceType, testNetwork1.network)
|
||||
val resolvedInfo = resolveService(discoveredInfo)
|
||||
|
||||
assertEquals(TEST_PORT, resolvedInfo.port)
|
||||
assertEquals(si1.hostname, resolvedInfo.hostname)
|
||||
assertAddressEquals(hostAddresses1, resolvedInfo.hostAddresses)
|
||||
|
||||
registerService(registrationRecord2, si2)
|
||||
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
|
||||
testNetwork1.network, Executor { it.run() }, discoveryRecord2)
|
||||
|
||||
val discoveredInfo2 = discoveryRecord2.waitForServiceDiscovered(
|
||||
serviceName2, serviceType, testNetwork1.network)
|
||||
val resolvedInfo2 = resolveService(discoveredInfo2)
|
||||
|
||||
assertEquals(TEST_PORT + 1, resolvedInfo2.port)
|
||||
assertEquals(si2.hostname, resolvedInfo2.hostname)
|
||||
assertAddressEquals(hostAddresses2, resolvedInfo2.hostAddresses)
|
||||
} cleanupStep {
|
||||
nsdManager.stopServiceDiscovery(discoveryRecord1)
|
||||
nsdManager.stopServiceDiscovery(discoveryRecord2)
|
||||
|
||||
discoveryRecord1.expectCallbackEventually<DiscoveryStopped>()
|
||||
discoveryRecord2.expectCallbackEventually<DiscoveryStopped>()
|
||||
} cleanup {
|
||||
nsdManager.unregisterService(registrationRecord1)
|
||||
nsdManager.unregisterService(registrationRecord2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAdvertisingAndDiscovery_multipleRegistrationsForSameCustomHost_unionOfAddressesFound() {
|
||||
val hostAddresses1 = listOf(
|
||||
parseNumericAddress("192.0.2.23"),
|
||||
parseNumericAddress("2001:db8::1"),
|
||||
parseNumericAddress("2001:db8::2"))
|
||||
val hostAddresses2 = listOf(
|
||||
parseNumericAddress("192.0.2.24"),
|
||||
parseNumericAddress("2001:db8::3"))
|
||||
val hostAddresses3 = listOf(
|
||||
parseNumericAddress("2001:db8::3"),
|
||||
parseNumericAddress("2001:db8::5"))
|
||||
val si1 = NsdServiceInfo().also {
|
||||
it.network = testNetwork1.network
|
||||
it.hostname = customHostname
|
||||
it.hostAddresses = hostAddresses1
|
||||
}
|
||||
val si2 = NsdServiceInfo().also {
|
||||
it.network = testNetwork1.network
|
||||
it.serviceName = serviceName
|
||||
it.serviceType = serviceType
|
||||
it.port = TEST_PORT
|
||||
it.hostname = customHostname
|
||||
it.hostAddresses = hostAddresses2
|
||||
}
|
||||
val si3 = NsdServiceInfo().also {
|
||||
it.network = testNetwork1.network
|
||||
it.serviceName = serviceName3
|
||||
it.serviceType = serviceType
|
||||
it.port = TEST_PORT + 1
|
||||
it.hostname = customHostname
|
||||
it.hostAddresses = hostAddresses3
|
||||
}
|
||||
|
||||
val registrationRecord1 = NsdRegistrationRecord()
|
||||
val registrationRecord2 = NsdRegistrationRecord()
|
||||
val registrationRecord3 = NsdRegistrationRecord()
|
||||
|
||||
val discoveryRecord = NsdDiscoveryRecord()
|
||||
tryTest {
|
||||
registerService(registrationRecord1, si1)
|
||||
registerService(registrationRecord2, si2)
|
||||
|
||||
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
|
||||
testNetwork1.network, Executor { it.run() }, discoveryRecord)
|
||||
|
||||
val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
|
||||
serviceName, serviceType, testNetwork1.network)
|
||||
val resolvedInfo1 = resolveService(discoveredInfo1)
|
||||
|
||||
assertEquals(TEST_PORT, resolvedInfo1.port)
|
||||
assertEquals(si1.hostname, resolvedInfo1.hostname)
|
||||
assertAddressEquals(
|
||||
hostAddresses1 + hostAddresses2,
|
||||
resolvedInfo1.hostAddresses)
|
||||
|
||||
registerService(registrationRecord3, si3)
|
||||
|
||||
val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
|
||||
serviceName3, serviceType, testNetwork1.network)
|
||||
val resolvedInfo2 = resolveService(discoveredInfo2)
|
||||
|
||||
assertEquals(TEST_PORT + 1, resolvedInfo2.port)
|
||||
assertEquals(si2.hostname, resolvedInfo2.hostname)
|
||||
assertAddressEquals(
|
||||
hostAddresses1 + hostAddresses2 + hostAddresses3,
|
||||
resolvedInfo2.hostAddresses)
|
||||
} cleanupStep {
|
||||
nsdManager.stopServiceDiscovery(discoveryRecord)
|
||||
|
||||
discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
|
||||
} cleanup {
|
||||
nsdManager.unregisterService(registrationRecord1)
|
||||
nsdManager.unregisterService(registrationRecord2)
|
||||
nsdManager.unregisterService(registrationRecord3)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAdvertisingAndDiscovery_servicesWithTheSameCustomHostAddressOmitted_addressesFound() {
|
||||
val hostAddresses = listOf(
|
||||
parseNumericAddress("192.0.2.23"),
|
||||
parseNumericAddress("2001:db8::1"),
|
||||
parseNumericAddress("2001:db8::2"))
|
||||
val si1 = NsdServiceInfo().also {
|
||||
it.network = testNetwork1.network
|
||||
it.serviceType = serviceType
|
||||
it.serviceName = serviceName
|
||||
it.port = TEST_PORT
|
||||
it.hostname = customHostname
|
||||
it.hostAddresses = hostAddresses
|
||||
}
|
||||
val si2 = NsdServiceInfo().also {
|
||||
it.network = testNetwork1.network
|
||||
it.serviceType = serviceType
|
||||
it.serviceName = serviceName2
|
||||
it.port = TEST_PORT + 1
|
||||
it.hostname = customHostname
|
||||
}
|
||||
|
||||
val registrationRecord1 = NsdRegistrationRecord()
|
||||
val registrationRecord2 = NsdRegistrationRecord()
|
||||
|
||||
val discoveryRecord = NsdDiscoveryRecord()
|
||||
tryTest {
|
||||
registerService(registrationRecord1, si1)
|
||||
|
||||
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
|
||||
testNetwork1.network, Executor { it.run() }, discoveryRecord)
|
||||
|
||||
val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
|
||||
serviceName, serviceType, testNetwork1.network)
|
||||
val resolvedInfo1 = resolveService(discoveredInfo1)
|
||||
|
||||
assertEquals(serviceName, discoveredInfo1.serviceName)
|
||||
assertEquals(TEST_PORT, resolvedInfo1.port)
|
||||
assertEquals(si1.hostname, resolvedInfo1.hostname)
|
||||
assertAddressEquals(hostAddresses, resolvedInfo1.hostAddresses)
|
||||
|
||||
registerService(registrationRecord2, si2)
|
||||
|
||||
val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
|
||||
serviceName2, serviceType, testNetwork1.network)
|
||||
val resolvedInfo2 = resolveService(discoveredInfo2)
|
||||
|
||||
assertEquals(serviceName2, discoveredInfo2.serviceName)
|
||||
assertEquals(TEST_PORT + 1, resolvedInfo2.port)
|
||||
assertEquals(si2.hostname, resolvedInfo2.hostname)
|
||||
assertAddressEquals(hostAddresses, resolvedInfo2.hostAddresses)
|
||||
} cleanupStep {
|
||||
nsdManager.stopServiceDiscovery(discoveryRecord)
|
||||
|
||||
discoveryRecord.expectCallback<DiscoveryStopped>()
|
||||
} cleanup {
|
||||
nsdManager.unregisterService(registrationRecord1)
|
||||
nsdManager.unregisterService(registrationRecord2)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildConflictingAnnouncement(): ByteBuffer {
|
||||
/*
|
||||
Generated with:
|
||||
|
|
@ -1463,6 +1798,22 @@ class NsdManagerTest {
|
|||
return buildMdnsPacket(mdnsPayload)
|
||||
}
|
||||
|
||||
private fun buildConflictingAnnouncementForCustomHost(): ByteBuffer {
|
||||
/*
|
||||
Generated with scapy:
|
||||
raw(DNS(rd=0, qr=1, aa=1, qd = None, an =
|
||||
DNSRR(rrname='NsdTestHost123456789.local', type=28, rclass=1, ttl=120,
|
||||
rdata='2001:db8::321')
|
||||
)).hex()
|
||||
*/
|
||||
val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000144e7364" +
|
||||
"54657374486f7374313233343536373839056c6f63616c00001c000100000078001020010db80000" +
|
||||
"00000000000000000321")
|
||||
replaceCustomHostnameWithTestSuffix(mdnsPayload)
|
||||
|
||||
return buildMdnsPacket(mdnsPayload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces occurrences of "NsdTest123456789" and "_nmt123456789" in mDNS payload with the
|
||||
* actual random name and type that are used by the test.
|
||||
|
|
@ -1479,6 +1830,19 @@ class NsdManagerTest {
|
|||
replaceAll(packetBuffer, testPacketTypePrefix, encodedTypePrefix)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces occurrences of "NsdTestHost123456789" in mDNS payload with the
|
||||
* actual random host name that are used by the test.
|
||||
*/
|
||||
private fun replaceCustomHostnameWithTestSuffix(mdnsPayload: ByteArray) {
|
||||
// Test custom hostnames have consistent length and are always ASCII
|
||||
val testPacketName = "NsdTestHost123456789".encodeToByteArray()
|
||||
val encodedHostname = customHostname.encodeToByteArray()
|
||||
|
||||
val packetBuffer = ByteBuffer.wrap(mdnsPayload)
|
||||
replaceAll(packetBuffer, testPacketName, encodedHostname)
|
||||
}
|
||||
|
||||
private tailrec fun replaceAll(buffer: ByteBuffer, source: ByteArray, replacement: ByteArray) {
|
||||
assertEquals(source.size, replacement.size)
|
||||
val index = buffer.array().indexOf(source)
|
||||
|
|
@ -1577,3 +1941,9 @@ private fun ByteArray?.utf8ToString(): String {
|
|||
if (this == null) return ""
|
||||
return String(this, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun assertAddressEquals(expected: List<InetAddress>, actual: List<InetAddress>) {
|
||||
// No duplicate addresses in the actual address list
|
||||
assertEquals(actual.toSet().size, actual.size)
|
||||
assertEquals(expected.toSet(), actual.toSet())
|
||||
}
|
||||
|
|
@ -52,6 +52,9 @@ import org.mockito.ArgumentCaptor;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
|
||||
@DevSdkIgnoreRunner.MonitorThreadLeak
|
||||
@RunWith(DevSdkIgnoreRunner.class)
|
||||
@SmallTest
|
||||
|
|
@ -370,6 +373,9 @@ public class NsdManagerTest {
|
|||
NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
|
||||
NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
|
||||
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
|
||||
NsdManager.RegistrationListener listener4 = mock(NsdManager.RegistrationListener.class);
|
||||
NsdManager.RegistrationListener listener5 = mock(NsdManager.RegistrationListener.class);
|
||||
NsdManager.RegistrationListener listener6 = mock(NsdManager.RegistrationListener.class);
|
||||
|
||||
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
|
||||
NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
|
||||
|
|
@ -379,6 +385,7 @@ public class NsdManagerTest {
|
|||
"_a_type._tcp,_sub1,_s2");
|
||||
NsdServiceInfo otherSubtypeUpdate = new NsdServiceInfo("a_name", "_a_type._tcp,_sub1,_s3");
|
||||
NsdServiceInfo dotSyntaxSubtypeUpdate = new NsdServiceInfo("a_name", "_sub1._a_type._tcp");
|
||||
|
||||
validService.setPort(2222);
|
||||
otherServiceWithSubtype.setPort(2222);
|
||||
validServiceDuplicate.setPort(2222);
|
||||
|
|
@ -386,6 +393,33 @@ public class NsdManagerTest {
|
|||
otherSubtypeUpdate.setPort(2222);
|
||||
dotSyntaxSubtypeUpdate.setPort(2222);
|
||||
|
||||
NsdServiceInfo invalidMissingHostnameWithAddresses = new NsdServiceInfo(null, null);
|
||||
invalidMissingHostnameWithAddresses.setHostAddresses(
|
||||
List.of(
|
||||
InetAddress.parseNumericAddress("192.168.82.14"),
|
||||
InetAddress.parseNumericAddress("2001::1")));
|
||||
|
||||
NsdServiceInfo validCustomHostWithAddresses = new NsdServiceInfo(null, null);
|
||||
validCustomHostWithAddresses.setHostname("a_host");
|
||||
validCustomHostWithAddresses.setHostAddresses(
|
||||
List.of(
|
||||
InetAddress.parseNumericAddress("192.168.82.14"),
|
||||
InetAddress.parseNumericAddress("2001::1")));
|
||||
|
||||
NsdServiceInfo validServiceWithCustomHostAndAddresses =
|
||||
new NsdServiceInfo("a_name", "_a_type._tcp");
|
||||
validServiceWithCustomHostAndAddresses.setPort(2222);
|
||||
validServiceWithCustomHostAndAddresses.setHostname("a_host");
|
||||
validServiceWithCustomHostAndAddresses.setHostAddresses(
|
||||
List.of(
|
||||
InetAddress.parseNumericAddress("192.168.82.14"),
|
||||
InetAddress.parseNumericAddress("2001::1")));
|
||||
|
||||
NsdServiceInfo validServiceWithCustomHostNoAddresses =
|
||||
new NsdServiceInfo("a_name", "_a_type._tcp");
|
||||
validServiceWithCustomHostNoAddresses.setPort(2222);
|
||||
validServiceWithCustomHostNoAddresses.setHostname("a_host");
|
||||
|
||||
// Service registration
|
||||
// - invalid arguments
|
||||
mustFail(() -> { manager.unregisterService(null); });
|
||||
|
|
@ -394,6 +428,8 @@ public class NsdManagerTest {
|
|||
mustFail(() -> { manager.registerService(invalidService, PROTOCOL, listener1); });
|
||||
mustFail(() -> { manager.registerService(validService, -1, listener1); });
|
||||
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
|
||||
mustFail(() -> {
|
||||
manager.registerService(invalidMissingHostnameWithAddresses, PROTOCOL, listener1); });
|
||||
manager.registerService(validService, PROTOCOL, listener1);
|
||||
// - update without subtype is not allowed
|
||||
mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
|
||||
|
|
@ -415,6 +451,15 @@ public class NsdManagerTest {
|
|||
// TODO: make listener immediately reusable
|
||||
//mustFail(() -> { manager.unregisterService(listener1); });
|
||||
//manager.registerService(validService, PROTOCOL, listener1);
|
||||
// - registering a custom host without a service is valid
|
||||
manager.registerService(validCustomHostWithAddresses, PROTOCOL, listener4);
|
||||
manager.unregisterService(listener4);
|
||||
// - registering a service with a custom host is valid
|
||||
manager.registerService(validServiceWithCustomHostAndAddresses, PROTOCOL, listener5);
|
||||
manager.unregisterService(listener5);
|
||||
// - registering a service with a custom host with no addresses is valid
|
||||
manager.registerService(validServiceWithCustomHostNoAddresses, PROTOCOL, listener6);
|
||||
manager.unregisterService(listener6);
|
||||
|
||||
// Discover service
|
||||
// - invalid arguments
|
||||
|
|
|
|||
Loading…
Reference in a new issue