Add APIs for discover/resolve on specific networks

Test that NsdManager specifies the correct network when a service is
discovered, and that services can be resolved on a specified network.
Also test that service discovery can be started on a specific network.

Bug: 190249673
Test: atest NsdManagerTest

Change-Id: Ie8b551ce9e33e3adf35f75508f91bbd0df71f837
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 0443456..b5d3bce 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -3,6 +3,7 @@
 
   public final class NsdManager {
     method public void discoverServices(String, int, android.net.nsd.NsdManager.DiscoveryListener);
+    method public void discoverServices(@NonNull String, int, @Nullable android.net.Network, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
     method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
     method public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
     method public void stopServiceDiscovery(android.net.nsd.NsdManager.DiscoveryListener);
@@ -43,12 +44,14 @@
     method public int describeContents();
     method public java.util.Map<java.lang.String,byte[]> getAttributes();
     method public java.net.InetAddress getHost();
+    method @Nullable public android.net.Network getNetwork();
     method public int getPort();
     method public String getServiceName();
     method public String getServiceType();
     method public void removeAttribute(String);
     method public void setAttribute(String, String);
     method public void setHost(java.net.InetAddress);
+    method public void setNetwork(@Nullable android.net.Network);
     method public void setPort(int);
     method public void setServiceName(String);
     method public void setServiceType(String);
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 23814c9..4c06a1e 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -15,6 +15,19 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.TestNetworkSpecifier
 import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
 import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
 import android.net.cts.NsdManagerTest.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
@@ -32,14 +45,27 @@
 import android.net.nsd.NsdManager.RegistrationListener
 import android.net.nsd.NsdManager.ResolveListener
 import android.net.nsd.NsdServiceInfo
+import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.net.module.util.TrackRecord
+import com.android.networkstack.apishim.ConstantsShim
+import com.android.networkstack.apishim.NsdShimImpl
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import org.junit.After
 import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.net.ServerSocket
@@ -57,12 +83,37 @@
 private const val TIMEOUT_MS = 2000L
 private const val DBG = false
 
+private val nsdShim = NsdShimImpl.newInstance()
+
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 @RunWith(AndroidJUnit4::class)
 class NsdManagerTest {
+    // NsdManager is not updatable before S, so tests do not need to be backwards compatible
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
+
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
     private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
+    private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
+
+    private lateinit var testNetwork1: TestTapNetwork
+    private lateinit var testNetwork2: TestTapNetwork
+
+    private class TestTapNetwork(
+        val iface: TestNetworkInterface,
+        val requestCb: NetworkCallback,
+        val agent: TestableNetworkAgent,
+        val network: Network
+    ) {
+        fun close(cm: ConnectivityManager) {
+            cm.unregisterNetworkCallback(requestCb)
+            agent.unregister()
+            iface.fileDescriptor.close()
+        }
+    }
 
     private interface NsdEvent
     private open class NsdRecord<T : NsdEvent> private constructor(
@@ -163,9 +214,14 @@
             add(ServiceLost(si))
         }
 
-        fun waitForServiceDiscovered(serviceName: String): NsdServiceInfo {
+        fun waitForServiceDiscovered(
+            serviceName: String,
+            expectedNetwork: Network? = null
+        ): NsdServiceInfo {
             return expectCallbackEventually<ServiceFound> {
-                it.serviceInfo.serviceName == serviceName
+                it.serviceInfo.serviceName == serviceName &&
+                        (expectedNetwork == null ||
+                                expectedNetwork == nsdShim.getNetwork(it.serviceInfo))
             }.serviceInfo
         }
     }
@@ -188,6 +244,52 @@
         }
     }
 
+    @Before
+    fun setUp() {
+        handlerThread.start()
+
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            testNetwork1 = createTestNetwork()
+            testNetwork2 = createTestNetwork()
+        }
+    }
+
+    private fun createTestNetwork(): TestTapNetwork {
+        val tnm = context.getSystemService(TestNetworkManager::class.java)
+        val iface = tnm.createTapInterface()
+        val cb = TestableNetworkCallback()
+        val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
+        cm.requestNetwork(NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(testNetworkSpecifier)
+                .build(), cb)
+        val agent = TestableNetworkAgent(context, handlerThread.looper,
+                NetworkCapabilities().apply {
+                    removeCapability(NET_CAPABILITY_TRUSTED)
+                    addTransportType(TRANSPORT_TEST)
+                    setNetworkSpecifier(testNetworkSpecifier)
+                },
+                LinkProperties().apply {
+                    interfaceName = iface.interfaceName
+                },
+                NetworkAgentConfig.Builder().build())
+        val network = agent.register()
+        agent.markConnected()
+        // The network has no INTERNET capability, so will be marked validated immediately
+        cb.expectAvailableThenValidatedCallbacks(network)
+        return TestTapNetwork(iface, cb, agent, network)
+    }
+
+    @After
+    fun tearDown() {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            testNetwork1.close(cm)
+            testNetwork2.close(cm)
+        }
+        handlerThread.quitSafely()
+    }
+
     @Test
     fun testNsdManager() {
         val si = NsdServiceInfo()
@@ -298,6 +400,84 @@
         registrationRecord2.expectCallback<ServiceUnregistered>()
     }
 
+    @Test
+    fun testNsdManager_DiscoverOnNetwork() {
+        // This tests requires shims supporting T+ APIs (discovering on specific network)
+        assumeTrue(ConstantsShim.VERSION > SC_V2)
+
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = this.serviceName
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        val registrationRecord = NsdRegistrationRecord()
+        val registeredInfo = registerService(registrationRecord, si)
+
+        tryTest {
+            val discoveryRecord = NsdDiscoveryRecord()
+            nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, discoveryRecord)
+
+            val foundInfo = discoveryRecord.waitForServiceDiscovered(
+                    serviceName, testNetwork1.network)
+            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
+
+            // Rewind to ensure the service is not found on the other interface
+            discoveryRecord.nextEvents.rewind(0)
+            assertNull(discoveryRecord.nextEvents.poll(timeoutMs = 100L) {
+                it is ServiceFound &&
+                        it.serviceInfo.serviceName == registeredInfo.serviceName &&
+                        nsdShim.getNetwork(it.serviceInfo) != testNetwork1.network
+            }, "The service should not be found on this network")
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testNsdManager_ResolveOnNetwork() {
+        // This tests requires shims supporting T+ APIs (NsdServiceInfo.network)
+        assumeTrue(ConstantsShim.VERSION > SC_V2)
+
+        val si = NsdServiceInfo()
+        si.serviceType = SERVICE_TYPE
+        si.serviceName = this.serviceName
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        val registrationRecord = NsdRegistrationRecord()
+        val registeredInfo = registerService(registrationRecord, si)
+        tryTest {
+            val resolveRecord = NsdResolveRecord()
+
+            val discoveryRecord = NsdDiscoveryRecord()
+            nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+
+            val foundInfo1 = discoveryRecord.waitForServiceDiscovered(
+                    serviceName, testNetwork1.network)
+            assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo1))
+            // Rewind as the service could be found on each interface in any order
+            discoveryRecord.nextEvents.rewind(0)
+            val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
+                    serviceName, testNetwork2.network)
+            assertEquals(testNetwork2.network, nsdShim.getNetwork(foundInfo2))
+
+            nsdManager.resolveService(foundInfo1, resolveRecord)
+            val cb = resolveRecord.expectCallback<ServiceResolved>()
+            cb.serviceInfo.let {
+                // Resolved service type has leading dot
+                assertEquals(".$SERVICE_TYPE", it.serviceType)
+                assertEquals(registeredInfo.serviceName, it.serviceName)
+                assertEquals(si.port, it.port)
+                assertEquals(testNetwork1.network, nsdShim.getNetwork(it))
+            }
+            // TODO: check that MDNS packets are sent only on testNetwork1.
+        } cleanupStep {
+            nsdManager.unregisterService(registrationRecord)
+        } cleanup {
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
     /**
      * Register a service and return its registration record.
      */
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index ca8cf07..e5e7ebc 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.net.Network;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -123,6 +124,7 @@
         fullInfo.setServiceType("_kitten._tcp");
         fullInfo.setPort(4242);
         fullInfo.setHost(LOCALHOST);
+        fullInfo.setNetwork(new Network(123));
         checkParcelable(fullInfo);
 
         NsdServiceInfo noHostInfo = new NsdServiceInfo();
@@ -172,6 +174,7 @@
         assertEquals(original.getServiceType(), result.getServiceType());
         assertEquals(original.getHost(), result.getHost());
         assertTrue(original.getPort() == result.getPort());
+        assertEquals(original.getNetwork(), result.getNetwork());
 
         // Assert equality of attribute map.
         Map<String, byte[]> originalMap = original.getAttributes();
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 6d1d765..5086943 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -218,14 +218,14 @@
         client.discoverServices("a_type", PROTOCOL, listener2);
         waitForIdle();
         verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommand("discover 3 a_type");
+        verifyDaemonCommand("discover 3 a_type 0");
 
         // Client resolve request
         NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
         client.resolveService(request, listener3);
         waitForIdle();
         verify(mDaemon, times(1)).maybeStart();
-        verifyDaemonCommand("resolve 4 a_name a_type local.");
+        verifyDaemonCommand("resolve 4 a_name a_type local. 0");
 
         // Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
         deathRecipient.binderDied();