From 6d618d77fe2bf1d67794a2950db43fd5d41d2410 Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Fri, 8 Dec 2023 15:49:00 +0800 Subject: [PATCH] [mdns] add support for advertising subtypes This commit adds support of advertising and discovering services with subtypes. With this change, client can publish a new service with subtypes with code: ``` NsdServiceInfo info = new NsdServiceInfo(); info.setServiceName("My Service"); info.setServiceType("_meshcop._udp"); info.setSubtypes(Set.of("_abc")); nsdManager.registerService(info, PROTOCOL_DNS_SD, listener); ``` Bug: 265095929 Test: atest CtsNetTestCases FrameworksNetTests Change-Id: Ia83182e2d43002243d1ef4357d14a8056d7da14b --- framework-t/api/current.txt | 2 + .../src/android/net/nsd/NsdManager.java | 2 + .../src/android/net/nsd/NsdServiceInfo.java | 39 ++++++++ .../android/net/nsd/NsdServiceInfoTest.java | 16 ++- .../net/src/android/net/cts/NsdManagerTest.kt | 99 +++++++++++++++++++ 5 files changed, 156 insertions(+), 2 deletions(-) diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt index 86745d401b..bbf518166b 100644 --- a/framework-t/api/current.txt +++ b/framework-t/api/current.txt @@ -251,6 +251,7 @@ package android.net.nsd { method public int getPort(); method public String getServiceName(); method public String getServiceType(); + method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") @NonNull public java.util.Set getSubtypes(); method public void removeAttribute(String); method public void setAttribute(String, String); method @Deprecated public void setHost(java.net.InetAddress); @@ -259,6 +260,7 @@ package android.net.nsd { method public void setPort(int); method public void setServiceName(String); method public void setServiceType(String); + method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void setSubtypes(@NonNull java.util.Set); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; } diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java index bf01a9d633..fcf79eb2b6 100644 --- a/framework-t/src/android/net/nsd/NsdManager.java +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -150,6 +150,8 @@ public final class NsdManager { public static class Flags { static final String REGISTER_NSD_OFFLOAD_ENGINE_API = "com.android.net.flags.register_nsd_offload_engine_api"; + static final String NSD_SUBTYPES_SUPPORT_ENABLED = + "com.android.net.flags.nsd_subtypes_support_enabled"; } /** diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java index caeecdda86..46c278a266 100644 --- a/framework-t/src/android/net/nsd/NsdServiceInfo.java +++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java @@ -16,6 +16,9 @@ package android.net.nsd; +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; @@ -24,6 +27,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Log; import com.android.net.module.util.InetAddressUtils; @@ -35,6 +39,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; /** * A class representing service information for network service discovery @@ -48,6 +53,8 @@ public final class NsdServiceInfo implements Parcelable { private String mServiceType; + private final Set mSubtypes = new ArraySet<>(); + private final ArrayMap mTxtRecord = new ArrayMap<>(); private final List mHostAddresses = new ArrayList<>(); @@ -391,11 +398,41 @@ public final class NsdServiceInfo implements Parcelable { mInterfaceIndex = interfaceIndex; } + /** + * Sets the subtypes to be advertised for this service instance. + * + * The elements in {@code subtypes} should be the subtype identifiers which have the trailing + * "._sub" removed. For example, the subtype should be "_printer" for + * "_printer._sub._http._tcp.local". + * + * Only one subtype will be registered if multiple elements of {@code subtypes} have the same + * case-insensitive value. + */ + @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED) + public void setSubtypes(@NonNull Set subtypes) { + mSubtypes.clear(); + mSubtypes.addAll(subtypes); + } + + /** + * Returns subtypes of this service instance. + * + * When this object is returned by the service discovery/browse APIs (etc. {@link + * NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this + * service. + */ + @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED) + @NonNull + public Set getSubtypes() { + return Collections.unmodifiableSet(mSubtypes); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("name: ").append(mServiceName) .append(", type: ").append(mServiceType) + .append(", subtypes: ").append(TextUtils.join(", ", mSubtypes)) .append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses)) .append(", port: ").append(mPort) .append(", network: ").append(mNetwork); @@ -414,6 +451,7 @@ public final class NsdServiceInfo implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeString(mServiceName); dest.writeString(mServiceType); + dest.writeStringList(new ArrayList<>(mSubtypes)); dest.writeInt(mPort); // TXT record key/value pairs. @@ -445,6 +483,7 @@ public final class NsdServiceInfo implements Parcelable { NsdServiceInfo info = new NsdServiceInfo(); info.mServiceName = in.readString(); info.mServiceType = in.readString(); + info.setSubtypes(new ArraySet<>(in.createStringArrayList())); info.mPort = in.readInt(); // TXT record key/value pairs. diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java index ffe0e9156d..79c4980a5c 100644 --- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java +++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java @@ -18,6 +18,7 @@ package android.net.nsd; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -40,6 +41,7 @@ import java.net.InetAddress; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; @RunWith(DevSdkIgnoreRunner.class) @SmallTest @@ -114,6 +116,7 @@ public class NsdServiceInfoTest { NsdServiceInfo fullInfo = new NsdServiceInfo(); fullInfo.setServiceName("kitten"); fullInfo.setServiceType("_kitten._tcp"); + fullInfo.setSubtypes(Set.of("_thread", "_matter")); fullInfo.setPort(4242); fullInfo.setHostAddresses(List.of(IPV4_ADDRESS)); fullInfo.setNetwork(new Network(123)); @@ -149,7 +152,7 @@ public class NsdServiceInfoTest { assertFalse(attributedInfo.getAttributes().keySet().contains("sticky")); } - public void checkParcelable(NsdServiceInfo original) { + private static void checkParcelable(NsdServiceInfo original) { // Write to parcel. Parcel p = Parcel.obtain(); Bundle writer = new Bundle(); @@ -179,11 +182,20 @@ public class NsdServiceInfoTest { } } - public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) { + private static void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) { byte[] txtRecord = shouldBeEmpty.getTxtRecord(); if (txtRecord == null || txtRecord.length == 0) { return; } fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord)); } + + @Test + public void testSubtypesValidSubtypesSuccess() { + NsdServiceInfo info = new NsdServiceInfo(); + + info.setSubtypes(Set.of("_thread", "_matter")); + + assertEquals(Set.of("_thread", "_matter"), info.getSubtypes()); + } } diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt index e1ea2b9a63..e0387c9616 100644 --- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt @@ -43,6 +43,7 @@ import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost +import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped @@ -991,6 +992,104 @@ class NsdManagerTest { } } + @Test + fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApi() { + runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = false) + } + + @Test + fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApiAndLegacySpecifier() { + runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = true) + } + + private fun runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier: Boolean) { + val si = makeTestServiceInfo(network = testNetwork1.network) + if (useLegacySpecifier) { + si.subtypes = setOf("_subtype1") + + // Test "_type._tcp.local,_subtype" syntax with the registration + si.serviceType = si.serviceType + ",_subtype2" + } else { + si.subtypes = setOf("_subtype1", "_subtype2") + } + + val registrationRecord = NsdRegistrationRecord() + + val baseTypeDiscoveryRecord = NsdDiscoveryRecord() + val subtype1DiscoveryRecord = NsdDiscoveryRecord() + val subtype2DiscoveryRecord = NsdDiscoveryRecord() + val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord() + tryTest { + registerService(registrationRecord, si) + + nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, + testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord) + + // Test "._type._tcp.local" syntax with discovery. Note this is not + // "._sub._type._tcp.local". + nsdManager.discoverServices("_othersubtype.$serviceType", + NsdManager.PROTOCOL_DNS_SD, + testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord) + nsdManager.discoverServices("_subtype1.$serviceType", + NsdManager.PROTOCOL_DNS_SD, + testNetwork1.network, Executor { it.run() }, subtype1DiscoveryRecord) + nsdManager.discoverServices("_subtype2.$serviceType", + NsdManager.PROTOCOL_DNS_SD, + testNetwork1.network, Executor { it.run() }, subtype2DiscoveryRecord) + + val info1 = subtype1DiscoveryRecord.waitForServiceDiscovered( + serviceName, serviceType, testNetwork1.network) + assertTrue(info1.subtypes.contains("_subtype1")) + val info2 = subtype2DiscoveryRecord.waitForServiceDiscovered( + serviceName, serviceType, testNetwork1.network) + assertTrue(info2.subtypes.contains("_subtype2")) + baseTypeDiscoveryRecord.waitForServiceDiscovered( + serviceName, serviceType, testNetwork1.network) + otherSubtypeDiscoveryRecord.expectCallback() + // The subtype callback was registered later but called, no need for an extra delay + otherSubtypeDiscoveryRecord.assertNoCallback(timeoutMs = 0) + } cleanupStep { + nsdManager.stopServiceDiscovery(baseTypeDiscoveryRecord) + nsdManager.stopServiceDiscovery(subtype1DiscoveryRecord) + nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord) + nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord) + + baseTypeDiscoveryRecord.expectCallback() + subtype1DiscoveryRecord.expectCallback() + subtype2DiscoveryRecord.expectCallback() + otherSubtypeDiscoveryRecord.expectCallback() + } cleanup { + nsdManager.unregisterService(registrationRecord) + } + } + + @Test + fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() { + val si = makeTestServiceInfo(network = testNetwork1.network) + // Sets 101 subtypes in total + val seq = generateSequence(1) { it + 1} + si.subtypes = seq.take(100).toList().map {it -> "_subtype" + it}.toSet() + si.serviceType = si.serviceType + ",_subtype" + + val record = NsdRegistrationRecord() + nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record) + + val failedCb = record.expectCallback(REGISTRATION_TIMEOUT_MS) + assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode) + } + + @Test + fun testSubtypeAdvertising_emptySubtypeLabel_returnsFailureBadParameters() { + val si = makeTestServiceInfo(network = testNetwork1.network) + si.subtypes = setOf("") + + val record = NsdRegistrationRecord() + nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record) + + val failedCb = record.expectCallback(REGISTRATION_TIMEOUT_MS) + assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode) + } + @Test fun testRegisterWithConflictDuringProbing() { // This test requires shims supporting T+ APIs (NsdServiceInfo.network)