Add mulit devices mdns tests

Add tests to verify mdns advertising, discovery, and resolution
over a hotspot connection using multiple devices.

Bug: 343311941
Test: atest CtsConnectivityMultiDevicesTestCases
Change-Id: I9f52a271385cddc12c8f57b188e080ad69651f9f
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index 7a326cd..abd6fe2 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -5,6 +5,7 @@
 from mobly import test_runner
 from mobly import utils
 from mobly.controllers import android_device
+from utils import mdns_utils
 from utils import tether_utils
 from utils.tether_utils import UpstreamType
 
@@ -61,6 +62,25 @@
           self.serverDevice, UpstreamType.CELLULAR
       )
 
+  def test_mdns_via_hotspot(self):
+    tether_utils.assume_hotspot_test_preconditions(
+        self.serverDevice, self.clientDevice, UpstreamType.NONE
+    )
+    try:
+      # Connectivity of the client verified by asserting the validated capability.
+      tether_utils.setup_hotspot_and_client_for_upstream_type(
+        self.serverDevice, self.clientDevice, UpstreamType.NONE
+      )
+      mdns_utils.register_mdns_service_and_discover_resolve(
+        self.clientDevice, self.serverDevice
+      )
+    finally:
+      mdns_utils.cleanup_mdns_service(
+        self.clientDevice, self.serverDevice
+      )
+      tether_utils.cleanup_tethering_for_upstream_type(
+        self.serverDevice, UpstreamType.NONE
+      )
 
 if __name__ == "__main__":
   # Take test args
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
index 5940cbb..b0b32c2 100644
--- a/tests/cts/multidevices/snippet/Android.bp
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -25,6 +25,7 @@
     ],
     srcs: [
         "ConnectivityMultiDevicesSnippet.kt",
+        "MdnsMultiDevicesSnippet.kt",
     ],
     manifest: "AndroidManifest.xml",
     static_libs: [
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
index 9ed8146..967e581 100644
--- a/tests/cts/multidevices/snippet/AndroidManifest.xml
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -27,7 +27,8 @@
          of a snippet class -->
     <meta-data
         android:name="mobly-snippets"
-        android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet" />
+        android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet,
+                       com.google.snippet.connectivity.MdnsMultiDevicesSnippet" />
   </application>
   <!-- Add an instrumentation tag so that the app can be launched through an
        instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
diff --git a/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
new file mode 100644
index 0000000..1b288df
--- /dev/null
+++ b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
@@ -0,0 +1,92 @@
+/*
+ * 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 com.google.snippet.connectivity
+
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import com.android.testutils.NsdRegistrationRecord
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import com.android.testutils.NsdResolveRecord
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ServiceResolved
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+import kotlin.test.assertEquals
+import org.junit.Assert.assertArrayEquals
+
+private const val SERVICE_NAME = "MultiDevicesTest"
+private const val SERVICE_TYPE = "_multi_devices._tcp"
+private const val SERVICE_ATTRIBUTES_KEY = "key"
+private const val SERVICE_ATTRIBUTES_VALUE = "value"
+private const val SERVICE_PORT = 12345
+private const val REGISTRATION_TIMEOUT_MS = 10_000L
+
+class MdnsMultiDevicesSnippet : Snippet {
+    private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+    private val nsdManager = context.getSystemService(NsdManager::class.java)!!
+    private val registrationRecord = NsdRegistrationRecord()
+    private val discoveryRecord = NsdDiscoveryRecord()
+    private val resolveRecord = NsdResolveRecord()
+
+    @Rpc(description = "Register a mDns service")
+    fun registerMDnsService() {
+        val info = NsdServiceInfo()
+        info.setServiceName(SERVICE_NAME)
+        info.setServiceType(SERVICE_TYPE)
+        info.setPort(SERVICE_PORT)
+        info.setAttribute(SERVICE_ATTRIBUTES_KEY, SERVICE_ATTRIBUTES_VALUE)
+        nsdManager.registerService(info, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+        registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+    }
+
+    @Rpc(description = "Unregister a mDns service")
+    fun unregisterMDnsService() {
+        nsdManager.unregisterService(registrationRecord)
+        registrationRecord.expectCallback<ServiceUnregistered>()
+    }
+
+    @Rpc(description = "Ensure the discovery and resolution of the mDNS service")
+    // Suppress the warning, as the NsdManager#resolveService() method is deprecated.
+    @Suppress("DEPRECATION")
+    fun ensureMDnsServiceDiscoveryAndResolution() {
+        // Discover a mDns service that matches the test service
+        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+        val info = discoveryRecord.waitForServiceDiscovered(SERVICE_NAME, SERVICE_TYPE)
+        // Resolve the retrieved mDns service.
+        nsdManager.resolveService(info, resolveRecord)
+        val serviceResolved = resolveRecord.expectCallbackEventually<ServiceResolved>()
+        serviceResolved.serviceInfo.let {
+            assertEquals(SERVICE_NAME, it.serviceName)
+            assertEquals(".$SERVICE_TYPE", it.serviceType)
+            assertEquals(SERVICE_PORT, it.port)
+            assertEquals(1, it.attributes.size)
+            assertArrayEquals(
+                    SERVICE_ATTRIBUTES_VALUE.encodeToByteArray(),
+                    it.attributes[SERVICE_ATTRIBUTES_KEY]
+            )
+        }
+    }
+
+    @Rpc(description = "Stop discovery")
+    fun stopMDnsServiceDiscovery() {
+        nsdManager.stopServiceDiscovery(discoveryRecord)
+        discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+    }
+}
diff --git a/tests/cts/multidevices/utils/mdns_utils.py b/tests/cts/multidevices/utils/mdns_utils.py
new file mode 100644
index 0000000..ec1fea0
--- /dev/null
+++ b/tests/cts/multidevices/utils/mdns_utils.py
@@ -0,0 +1,42 @@
+#  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.
+
+from mobly.controllers import android_device
+
+
+def register_mdns_service_and_discover_resolve(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  """Test mdns advertising, discovery and resolution
+
+  One device registers an mDNS service, and another device discovers and
+  resolves that service.
+  """
+  advertising = advertising_device.connectivity_multi_devices_snippet
+  discovery = discovery_device.connectivity_multi_devices_snippet
+
+  # Register a mDns service
+  advertising.registerMDnsService()
+
+  # Ensure the discovery and resolution of the mDNS service
+  discovery.ensureMDnsServiceDiscoveryAndResolution()
+
+
+def cleanup_mdns_service(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  # Unregister the mDns service
+  advertising_device.connectivity_multi_devices_snippet.unregisterMDnsService()
+  # Stop discovery
+  discovery_device.connectivity_multi_devices_snippet.stopMDnsServiceDiscovery()
diff --git a/tests/cts/multidevices/utils/tether_utils.py b/tests/cts/multidevices/utils/tether_utils.py
index b37ed76..702b596 100644
--- a/tests/cts/multidevices/utils/tether_utils.py
+++ b/tests/cts/multidevices/utils/tether_utils.py
@@ -20,6 +20,7 @@
 
 
 class UpstreamType:
+  NONE = 0
   CELLULAR = 1
   WIFI = 2
 
@@ -56,6 +57,8 @@
         not server.isStaApConcurrencySupported(),
         "Server requires Wifi AP + STA concurrency",
     )
+  elif upstream_type == UpstreamType.NONE:
+    pass
   else:
     raise ValueError(f"Invalid upstream type: {upstream_type}")
 
@@ -78,6 +81,8 @@
     server.requestCellularAndEnsureDefault()
   elif upstream_type == UpstreamType.WIFI:
     server.ensureWifiIsDefault()
+  elif upstream_type == UpstreamType.NONE:
+    pass
   else:
     raise ValueError(f"Invalid upstream type: {upstream_type}")