Merge changes I44e85c09,I01810b54,I1c169311,I15f2aeac,I048512a1 into main

* changes:
  Use AutoCloseTestInterfaceRule in NsdManagerDownstreamTetheringTest
  Add auto close rule for test interfaces
  Add finalizer to check whether destroy was called
  Wrap use of test interface in EthernetTestInterface
  Add a class to properly manage ethernet test interfaces
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
new file mode 100644
index 0000000..89de0b3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.android.testutils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class AutoCloseTestInterfaceRule(
+        private val context: Context,
+    ) : TestRule {
+    private val tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+        context.getSystemService(TestNetworkManager::class.java)!!
+    }
+    private val ifaces = ArrayList<TestNetworkInterface>()
+
+    fun createTapInterface(): TestNetworkInterface {
+        return runAsShell(MANAGE_TEST_NETWORKS) {
+            tnm.createTapInterface()
+        }.also {
+            ifaces.add(it)
+        }
+    }
+
+    private fun closeAllInterfaces() {
+        // TODO: wait on RTM_DELLINK before proceeding.
+        for (iface in ifaces) {
+            // ParcelFileDescriptor prevents the fd from being double closed.
+            iface.getFileDescriptor().close()
+        }
+    }
+
+    private inner class AutoCloseTestInterfaceRuleStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            tryTest {
+                base.evaluate()
+            } cleanup {
+                closeAllInterfaces()
+            }
+        }
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return AutoCloseTestInterfaceRuleStatement(base, description)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f45f881..786023c 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -17,14 +17,17 @@
 
 import android.net.EthernetTetheringTestBase
 import android.net.LinkAddress
-import android.net.TestNetworkInterface
 import android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL
 import android.net.TetheringManager.TETHERING_ETHERNET
 import android.net.TetheringManager.TetheringRequest
+import android.net.cts.util.EthernetTestInterface
 import android.net.nsd.NsdManager
 import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import androidx.test.filters.SmallTest
+import com.android.testutils.AutoCloseTestInterfaceRule
 import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
@@ -38,9 +41,12 @@
 import org.junit.After
 import org.junit.Assume.assumeFalse
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
+private const val TAG = "NsdManagerDownstreamTetheringTest"
+
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @ConnectivityModuleTest
@@ -50,14 +56,27 @@
     private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
     private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
 
+    private val handlerThread = HandlerThread("$TAG thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+    private lateinit var downstreamIface: EthernetTestInterface
+
+    @get:Rule
+    val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
     @Before
     override fun setUp() {
         super.setUp()
-        setIncludeTestInterfaces(true)
+        val iface = testInterfaceRule.createTapInterface()
+        downstreamIface = EthernetTestInterface(context, handler, iface)
     }
 
     @After
     override fun tearDown() {
+        if (::downstreamIface.isInitialized) {
+            downstreamIface.destroy()
+        }
+        handlerThread.quitSafely()
+        handlerThread.join()
         super.tearDown()
     }
 
@@ -65,16 +84,14 @@
     fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
         assumeFalse(isInterfaceForTetheringAvailable())
 
-        var downstreamIface: TestNetworkInterface? = null
         var tetheringEventCallback: MyTetheringEventCallback? = null
         var downstreamReader: TapPacketReader? = null
 
         val discoveryRecord = NsdDiscoveryRecord()
 
         tryTest {
-            downstreamIface = createTestInterface()
             val iface = mTetheredInterfaceRequester.getInterface()
-            assertEquals(iface, downstreamIface?.interfaceName)
+            assertEquals(downstreamIface.name, iface)
             val request = TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
             tetheringEventCallback = enableEthernetTethering(
@@ -85,7 +102,7 @@
             }
             // This shouldn't be flaky because the TAP interface will buffer all packets even
             // before the reader is started.
-            downstreamReader = makePacketReader(downstreamIface)
+            downstreamReader = makePacketReader(downstreamIface.testIface)
             waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS)
 
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
@@ -96,8 +113,6 @@
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
         } cleanupStep {
             maybeStopTapPacketReader(downstreamReader)
-        } cleanupStep {
-            maybeCloseTestInterface(downstreamIface)
         } cleanup {
             maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         }
@@ -107,16 +122,14 @@
     fun testMdnsDiscoveryWorkOnTetheringInterface() {
         assumeFalse(isInterfaceForTetheringAvailable())
 
-        var downstreamIface: TestNetworkInterface? = null
         var tetheringEventCallback: MyTetheringEventCallback? = null
         var downstreamReader: TapPacketReader? = null
 
         val discoveryRecord = NsdDiscoveryRecord()
 
         tryTest {
-            downstreamIface = createTestInterface()
             val iface = mTetheredInterfaceRequester.getInterface()
-            assertEquals(iface, downstreamIface?.interfaceName)
+            assertEquals(downstreamIface.name, iface)
 
             val localAddr = LinkAddress("192.0.2.3/28")
             val clientAddr = LinkAddress("192.0.2.2/28")
@@ -130,9 +143,9 @@
                 awaitInterfaceTethered()
             }
 
-            val fd = downstreamIface?.fileDescriptor?.fileDescriptor
+            val fd = downstreamIface.testIface.fileDescriptor?.fileDescriptor
             assertNotNull(fd)
-            downstreamReader = makePacketReader(fd, getMTU(downstreamIface))
+            downstreamReader = makePacketReader(fd, getMTU(downstreamIface.testIface))
 
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
@@ -143,8 +156,6 @@
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
         } cleanupStep {
             maybeStopTapPacketReader(downstreamReader)
-        } cleanupStep {
-            maybeCloseTestInterface(downstreamIface)
         } cleanup {
             maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         }
diff --git a/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
new file mode 100644
index 0000000..a93430c
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.cts.util
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.IpConfiguration
+import android.net.TestNetworkInterface
+import android.net.cts.util.EthernetTestInterface.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Handler
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.runAsShell
+import kotlin.test.assertNotNull
+
+private const val TAG = "EthernetTestInterface"
+private const val TIMEOUT_MS = 5_000L
+
+/**
+ * A class to manage the lifecycle of an ethernet interface.
+ *
+ * This class encapsulates creating new tun/tap interfaces and registering them with ethernet
+ * service.
+ */
+class EthernetTestInterface(
+    private val context: Context,
+    private val handler: Handler,
+    val testIface: TestNetworkInterface
+) {
+    private class EthernetStateListener(private val trackedIface: String) : InterfaceStateListener {
+        val events = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+        sealed class CallbackEntry {
+            data class InterfaceStateChanged(
+                val iface: String,
+                val state: Int,
+                val role: Int,
+                val cfg: IpConfiguration?
+            ) : CallbackEntry()
+        }
+
+        override fun onInterfaceStateChanged(
+            iface: String,
+            state: Int,
+            role: Int,
+            cfg: IpConfiguration?
+        ) {
+            // filter out callbacks for other interfaces
+            if (iface != trackedIface) return
+            events.add(InterfaceStateChanged(iface, state, role, cfg))
+        }
+
+        fun eventuallyExpect(state: Int) {
+            val cb = events.poll(TIMEOUT_MS) { it is InterfaceStateChanged && it.state == state }
+            assertNotNull(cb, "Never received state $state. Got: ${events.backtrace()}")
+        }
+    }
+
+    val name get() = testIface.interfaceName
+    private val listener = EthernetStateListener(name)
+    private val em = context.getSystemService(EthernetManager::class.java)!!
+    private var cleanedUp = false
+
+    init{
+        em.addInterfaceStateListener(handler::post, listener)
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(true)
+        }
+        // Wait for link up to be processed in EthernetManager before returning.
+        listener.eventuallyExpect(STATE_LINK_UP)
+    }
+
+    fun destroy() {
+        // It is possible that the fd was already closed by the test, in which case this is a noop.
+        testIface.getFileDescriptor().close()
+        listener.eventuallyExpect(STATE_ABSENT)
+
+        // setIncludeTestInterfaces() posts on the handler and does not run synchronously. However,
+        // there should be no need for a synchronization mechanism here. If the next test is
+        // bringing up its interface, a RTM_NEWLINK will be put on the back of the handler and is
+        // guaranteed to be in order with (i.e. after) this call, so there is no race here.
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(false)
+        }
+        em.removeInterfaceStateListener(listener)
+
+        cleanedUp = true
+    }
+
+    protected fun finalize() {
+        if (!cleanedUp) {
+            Log.wtf(TAG, "destroy() was not called for interface $name.")
+            destroy()
+        }
+    }
+}