Move net-test-utils to its own directory

Having the test targets in a different directory allows setting
visibility rules for tests only, which is necessary for access to
targets that should not be used for device builds.

Bug: 182859030
Test: m
Change-Id: Iaf426cf339a97833acf80c941db692329c6e2dcb
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
new file mode 100644
index 0000000..bea5e5a
--- /dev/null
+++ b/staticlibs/testutils/Android.bp
@@ -0,0 +1,51 @@
+// Copyright (C) 2021 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.
+
+java_library {
+    name: "net-tests-utils",
+    srcs: [
+        "devicetests/**/*.java",
+        "devicetests/**/*.kt",
+    ],
+    defaults: ["lib_mockito_extended"],
+    libs: [
+        "androidx.annotation_annotation",
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "kotlin-reflect",
+        "libnanohttpd",
+        "net-tests-utils-host-device-common",
+        "net-utils-device-common",
+    ],
+}
+
+java_library {
+  // Consider using net-tests-utils instead if writing device code.
+  // That library has a lot more useful tools into it for users that
+  // work on Android and includes this lib.
+  name: "net-tests-utils-host-device-common",
+  srcs: [
+      "hostdevice/**/*.java",
+      "hostdevice/**/*.kt",
+  ],
+  host_supported: true,
+  visibility: [
+      "//frameworks/libs/net/common/tests:__subpackages__",
+      "//frameworks/libs/net/client-libs/tests:__subpackages__",
+  ],
+  static_libs: [
+      "kotlin-test"
+  ]
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt b/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt
new file mode 100644
index 0000000..86631c3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 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.net.MacAddress
+import java.net.Inet4Address
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+private val TYPE_ARP = byteArrayOf(0x08, 0x06)
+// Arp reply header for IPv4 over ethernet
+private val ARP_REPLY_IPV4 = byteArrayOf(0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x02)
+
+/**
+ * A class that can be used to reply to ARP packets on a [TapPacketReader].
+ */
+class ArpResponder(
+    reader: TapPacketReader,
+    table: Map<Inet4Address, MacAddress>,
+    name: String = ArpResponder::class.java.simpleName
+) : PacketResponder(reader, ArpRequestFilter(), name) {
+    // Copy the map if not already immutable (toMap) to make sure it is not modified
+    private val table = table.toMap()
+
+    override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
+        val targetIp = InetAddress.getByAddress(
+                packet.copyFromIndexWithLength(ARP_TARGET_IPADDR_OFFSET, 4))
+                as Inet4Address
+
+        val macAddr = table[targetIp]?.toByteArray() ?: return
+        val senderMac = packet.copyFromIndexWithLength(ARP_SENDER_MAC_OFFSET, 6)
+        reader.sendResponse(ByteBuffer.wrap(
+                // Ethernet header
+                senderMac + macAddr + TYPE_ARP +
+                        // ARP message
+                        ARP_REPLY_IPV4 +
+                        macAddr /* sender MAC */ +
+                        targetIp.address /* sender IP addr */ +
+                        macAddr /* target mac */ +
+                        targetIp.address /* target IP addr */
+        ))
+    }
+}
+
+private fun ByteArray.copyFromIndexWithLength(start: Int, len: Int) =
+        copyOfRange(start, start + len)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
new file mode 100644
index 0000000..ff8c668
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/CompatUtil.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 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.net.NetworkSpecifier
+import android.os.Build
+
+/**
+ * Test utility to create [NetworkSpecifier]s on different SDK versions.
+ */
+object CompatUtil {
+    @JvmStatic
+    fun makeTestNetworkSpecifier(ifName: String): NetworkSpecifier {
+        // Until R, there was no TestNetworkSpecifier, StringNetworkSpecifier was used instead
+        if (isDevSdkInRange(minExclusive = null, maxInclusive = Build.VERSION_CODES.R)) {
+            return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
+        }
+        // TestNetworkSpecifier is not part of the SDK in some branches using this utility
+        // TODO: replace with a direct call to the constructor
+        return makeNetworkSpecifierInternal("android.net.TestNetworkSpecifier", ifName)
+    }
+
+    @JvmStatic
+    fun makeEthernetNetworkSpecifier(ifName: String): NetworkSpecifier {
+        // Until R, there was no EthernetNetworkSpecifier, StringNetworkSpecifier was used instead
+        if (isDevSdkInRange(minExclusive = null, maxInclusive = Build.VERSION_CODES.R)) {
+            return makeNetworkSpecifierInternal("android.net.StringNetworkSpecifier", ifName)
+        }
+        // EthernetNetworkSpecifier is not part of the SDK in some branches using this utility
+        // TODO: replace with a direct call to the constructor
+        return makeNetworkSpecifierInternal("android.net.EthernetNetworkSpecifier", ifName)
+    }
+
+    private fun makeNetworkSpecifierInternal(clazz: String, specifier: String): NetworkSpecifier {
+        // StringNetworkSpecifier was removed after R (and was hidden API before that)
+        return Class.forName(clazz)
+                .getConstructor(String::class.java).newInstance(specifier) as NetworkSpecifier
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
new file mode 100644
index 0000000..98464eb
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
@@ -0,0 +1,188 @@
+package com.android.testutils
+
+import android.os.SystemClock
+import java.util.concurrent.CyclicBarrier
+import kotlin.system.measureTimeMillis
+import kotlin.test.assertEquals
+import kotlin.test.assertFails
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+// The table contains pairs associating a regexp with the code to run. The statement is matched
+// against each matcher in sequence and when a match is found the associated code is run, passing
+// it the TrackRecord under test and the result of the regexp match.
+typealias InterpretMatcher<T> = Pair<Regex, (ConcurrentInterpreter<T>, T, MatchResult) -> Any?>
+
+// The default unit of time for interpreted tests
+val INTERPRET_TIME_UNIT = 40L // ms
+
+/**
+ * A small interpreter for testing parallel code. The interpreter will read a list of lines
+ * consisting of "|"-separated statements. Each column runs in a different concurrent thread
+ * and all threads wait for each other in between lines. Each statement is split on ";" then
+ * matched with regular expressions in the instructionTable constant, which contains the
+ * code associated with each statement. The interpreter supports an object being passed to
+ * the interpretTestSpec() method to be passed in each lambda (think about the object under
+ * test), and an optional transform function to be executed on the object at the start of
+ * every thread.
+ *
+ * The time unit is defined in milliseconds by the interpretTimeUnit member, which has a default
+ * value but can be passed to the constructor. Whitespace is ignored.
+ *
+ * The interpretation table has to be passed as an argument. It's a table associating a regexp
+ * with the code that should execute, as a function taking three arguments : the interpreter,
+ * the regexp match, and the object. See the individual tests for the DSL of that test.
+ * Implementors for new interpreting languages are encouraged to look at the defaultInterpretTable
+ * constant below for an example of how to write an interpreting table.
+ * Some expressions already exist by default and can be used by all interpreters. They include :
+ * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
+ * EXPR = VALUE : asserts that EXPR equals VALUE. EXPR is interpreted. VALUE can either be the
+ *   string "null" or an int. Returns Unit.
+ * EXPR time x..y : measures the time taken by EXPR and asserts it took at least x and at most
+ *   y time units.
+ * EXPR // any text : comments are ignored.
+ */
+open class ConcurrentInterpreter<T>(
+    localInterpretTable: List<InterpretMatcher<T>>,
+    val interpretTimeUnit: Long = INTERPRET_TIME_UNIT
+) {
+    private val interpretTable: List<InterpretMatcher<T>> =
+            localInterpretTable + getDefaultInstructions()
+
+    // Split the line into multiple statements separated by ";" and execute them. Return whatever
+    // the last statement returned.
+    fun interpretMultiple(instr: String, r: T): Any? {
+        return instr.split(";").map { interpret(it.trim(), r) }.last()
+    }
+
+    // Match the statement to a regex and interpret it.
+    fun interpret(instr: String, r: T): Any? {
+        val (matcher, code) =
+                interpretTable.find { instr matches it.first } ?: throw SyntaxException(instr)
+        val match = matcher.matchEntire(instr) ?: throw SyntaxException(instr)
+        return code(this, r, match)
+    }
+
+    // Spins as many threads as needed by the test spec and interpret each program concurrently,
+    // having all threads waiting on a CyclicBarrier after each line.
+    // |lineShift| says how many lines after the call the spec starts. This is used for error
+    // reporting. Unfortunately AFAICT there is no way to get the line of an argument rather
+    // than the line at which the expression starts.
+    fun interpretTestSpec(
+        spec: String,
+        initial: T,
+        lineShift: Int = 0,
+        threadTransform: (T) -> T = { it }
+    ) {
+        // For nice stack traces
+        val callSite = getCallingMethod()
+        val lines = spec.trim().trim('\n').split("\n").map { it.split("|") }
+        // |threads| contains arrays of strings that make up the statements of a thread : in other
+        // words, it's an array that contains a list of statements for each column in the spec.
+        val threadCount = lines[0].size
+        assertTrue(lines.all { it.size == threadCount })
+        val threadInstructions = (0 until threadCount).map { i -> lines.map { it[i].trim() } }
+        val barrier = CyclicBarrier(threadCount)
+        var crash: InterpretException? = null
+        threadInstructions.mapIndexed { threadIndex, instructions ->
+            Thread {
+                val threadLocal = threadTransform(initial)
+                barrier.await()
+                var lineNum = 0
+                instructions.forEach {
+                    if (null != crash) return@Thread
+                    lineNum += 1
+                    try {
+                        interpretMultiple(it, threadLocal)
+                    } catch (e: Throwable) {
+                        // If fail() or some exception was called, the thread will come here ; if
+                        // the exception isn't caught the process will crash, which is not nice for
+                        // testing. Instead, catch the exception, cancel other threads, and report
+                        // nicely. Catch throwable because fail() is AssertionError, which inherits
+                        // from Error.
+                        crash = InterpretException(threadIndex, it,
+                                callSite.lineNumber + lineNum + lineShift,
+                                callSite.className, callSite.methodName, callSite.fileName, e)
+                    }
+                    barrier.await()
+                }
+            }.also { it.start() }
+        }.forEach { it.join() }
+        // If the test failed, crash with line number
+        crash?.let { throw it }
+    }
+
+    // Helper to get the stack trace for a calling method
+    private fun getCallingStackTrace(): Array<StackTraceElement> {
+        try {
+            throw RuntimeException()
+        } catch (e: RuntimeException) {
+            return e.stackTrace
+        }
+    }
+
+    // Find the calling method. This is the first method in the stack trace that is annotated
+    // with @Test.
+    fun getCallingMethod(): StackTraceElement {
+        val stackTrace = getCallingStackTrace()
+        return stackTrace.find { element ->
+            val clazz = Class.forName(element.className)
+            // Because the stack trace doesn't list the formal arguments, find all methods with
+            // this name and return this name if any of them is annotated with @Test.
+            clazz.declaredMethods
+                    .filter { method -> method.name == element.methodName }
+                    .any { method -> method.getAnnotation(org.junit.Test::class.java) != null }
+        } ?: stackTrace[3]
+        // If no method is annotated return the 4th one, because that's what it usually is :
+        // 0 is getCallingStackTrace, 1 is this method, 2 is ConcurrentInterpreter#interpretTestSpec
+    }
+}
+
+private fun <T> getDefaultInstructions() = listOf<InterpretMatcher<T>>(
+    // Interpret an empty line as doing nothing.
+    Regex("") to { _, _, _ -> null },
+    // Ignore comments.
+    Regex("(.*)//.*") to { i, t, r -> i.interpret(r.strArg(1), t) },
+    // Interpret "XXX time x..y" : run XXX and check it took at least x and not more than y
+    Regex("""(.*)\s*time\s*(\d+)\.\.(\d+)""") to { i, t, r ->
+        val time = measureTimeMillis { i.interpret(r.strArg(1), t) }
+        assertTrue(time in r.timeArg(2)..r.timeArg(3), "$time not in ${r.timeArg(2)..r.timeArg(3)}")
+    },
+    // Interpret "XXX = YYY" : run XXX and assert its return value is equal to YYY. "null" supported
+    Regex("""(.*)\s*=\s*(null|\d+)""") to { i, t, r ->
+        i.interpret(r.strArg(1), t).also {
+            if ("null" == r.strArg(2)) assertNull(it) else assertEquals(r.intArg(2), it)
+        }
+    },
+    // Interpret sleep. Optional argument for the count, in INTERPRET_TIME_UNIT units.
+    Regex("""sleep(\((\d+)\))?""") to { i, t, r ->
+        SystemClock.sleep(if (r.strArg(2).isEmpty()) i.interpretTimeUnit else r.timeArg(2))
+    },
+    Regex("""(.*)\s*fails""") to { i, t, r ->
+        assertFails { i.interpret(r.strArg(1), t) }
+    }
+)
+
+class SyntaxException(msg: String, cause: Throwable? = null) : RuntimeException(msg, cause)
+class InterpretException(
+    threadIndex: Int,
+    instr: String,
+    lineNum: Int,
+    className: String,
+    methodName: String,
+    fileName: String,
+    cause: Throwable
+) : RuntimeException("Failure: $instr", cause) {
+    init {
+        stackTrace = arrayOf(StackTraceElement(
+                className,
+                "$methodName:thread$threadIndex",
+                fileName,
+                lineNum)) + super.getStackTrace()
+    }
+}
+
+// Some small helpers to avoid to say the large ".groupValues[index].trim()" every time
+fun MatchResult.strArg(index: Int) = this.groupValues[index].trim()
+fun MatchResult.intArg(index: Int) = strArg(index).toInt()
+fun MatchResult.timeArg(index: Int) = INTERPRET_TIME_UNIT * intArg(index)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
new file mode 100644
index 0000000..4a83f6f
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 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.os.Build
+import org.junit.Assume.assumeTrue
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Returns true if the development SDK version of the device is in the provided range.
+ *
+ * If the device is not using a release SDK, the development SDK is considered to be higher than
+ * [Build.VERSION.SDK_INT].
+ */
+fun isDevSdkInRange(minExclusive: Int?, maxInclusive: Int?): Boolean {
+    // In-development API n+1 will have SDK_INT == n and CODENAME != REL.
+    // Stable API n has SDK_INT == n and CODENAME == REL.
+    val release = "REL" == Build.VERSION.CODENAME
+    val sdkInt = Build.VERSION.SDK_INT
+    val devApiLevel = sdkInt + if (release) 0 else 1
+
+    return (minExclusive == null || devApiLevel > minExclusive) &&
+            (maxInclusive == null || devApiLevel <= maxInclusive)
+}
+
+/**
+ * A test rule to ignore tests based on the development SDK level.
+ *
+ * If the device is not using a release SDK, the development SDK is considered to be higher than
+ * [Build.VERSION.SDK_INT].
+ *
+ * @param ignoreClassUpTo Skip all tests in the class if the device dev SDK is <= this value.
+ * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this value.
+ */
+class DevSdkIgnoreRule @JvmOverloads constructor(
+    private val ignoreClassUpTo: Int? = null,
+    private val ignoreClassAfter: Int? = null
+) : TestRule {
+    override fun apply(base: Statement, description: Description): Statement {
+        return IgnoreBySdkStatement(base, description)
+    }
+
+    /**
+     * Ignore the test for any development SDK that is strictly after [value].
+     *
+     * If the device is not using a release SDK, the development SDK is considered to be higher
+     * than [Build.VERSION.SDK_INT].
+     */
+    annotation class IgnoreAfter(val value: Int)
+
+    /**
+     * Ignore the test for any development SDK that lower than or equal to [value].
+     *
+     * If the device is not using a release SDK, the development SDK is considered to be higher
+     * than [Build.VERSION.SDK_INT].
+     */
+    annotation class IgnoreUpTo(val value: Int)
+
+    private inner class IgnoreBySdkStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            val ignoreAfter = description.getAnnotation(IgnoreAfter::class.java)
+            val ignoreUpTo = description.getAnnotation(IgnoreUpTo::class.java)
+
+            val message = "Skipping test for build ${Build.VERSION.CODENAME} " +
+                    "with SDK ${Build.VERSION.SDK_INT}"
+            assumeTrue(message, isDevSdkInRange(ignoreClassUpTo, ignoreClassAfter))
+            assumeTrue(message, isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value))
+            base.evaluate()
+        }
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
new file mode 100644
index 0000000..73b2843
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import org.junit.runner.Description
+import org.junit.runner.Runner
+import org.junit.runner.notification.RunNotifier
+
+/**
+ * A runner that can skip tests based on the development SDK as defined in [DevSdkIgnoreRule].
+ *
+ * Generally [DevSdkIgnoreRule] should be used for that purpose (using rules is preferable over
+ * replacing the test runner), however JUnit runners inspect all methods in the test class before
+ * processing test rules. This may cause issues if the test methods are referencing classes that do
+ * not exist on the SDK of the device the test is run on.
+ *
+ * This runner inspects [IgnoreAfter] and [IgnoreUpTo] annotations on the test class, and will skip
+ * the whole class if they do not match the development SDK as defined in [DevSdkIgnoreRule].
+ * Otherwise, it will delegate to [AndroidJUnit4] to run the test as usual.
+ *
+ * Example usage:
+ *
+ *     @RunWith(DevSdkIgnoreRunner::class)
+ *     @IgnoreUpTo(Build.VERSION_CODES.Q)
+ *     class MyTestClass { ... }
+ */
+class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner() {
+    private val baseRunner = klass.let {
+        val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
+        val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
+
+        if (isDevSdkInRange(ignoreUpTo?.value, ignoreAfter?.value)) AndroidJUnit4(klass) else null
+    }
+
+    override fun run(notifier: RunNotifier) {
+        if (baseRunner != null) {
+            baseRunner.run(notifier)
+            return
+        }
+
+        // Report a single, skipped placeholder test for this class, so that the class is still
+        // visible as skipped in test results.
+        notifier.fireTestIgnored(
+                Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+    }
+
+    override fun getDescription(): Description {
+        return baseRunner?.description ?: Description.createSuiteDescription(klass)
+    }
+
+    override fun testCount(): Int {
+        // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
+        return baseRunner?.testCount() ?: 1
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
new file mode 100644
index 0000000..825d748
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 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.net.DnsResolver
+import android.net.InetAddresses
+import android.os.Looper
+import android.os.Handler
+import com.android.internal.annotations.GuardedBy
+import java.net.InetAddress
+import java.util.concurrent.Executor
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
+
+const val TYPE_UNSPECIFIED = -1
+// TODO: Integrate with NetworkMonitorTest.
+class FakeDns(val mockResolver: DnsResolver) {
+    class DnsEntry(val hostname: String, val type: Int, val addresses: List<InetAddress>) {
+        fun match(host: String, type: Int) = hostname.equals(host) && type == type
+    }
+
+    @GuardedBy("answers")
+    val answers = ArrayList<DnsEntry>()
+
+    fun getAnswer(hostname: String, type: Int): DnsEntry? = synchronized(answers) {
+        return answers.firstOrNull { it.match(hostname, type) }
+    }
+
+    fun setAnswer(hostname: String, answer: Array<String>, type: Int) = synchronized(answers) {
+        val ans = DnsEntry(hostname, type, generateAnswer(answer))
+        // Replace or remove the existing one.
+        when (val index = answers.indexOfFirst { it.match(hostname, type) }) {
+            -1 -> answers.add(ans)
+            else -> answers[index] = ans
+        }
+    }
+
+    private fun generateAnswer(answer: Array<String>) =
+            answer.filterNotNull().map { InetAddresses.parseNumericAddress(it) }
+
+    fun startMocking() {
+        // Mock DnsResolver.query() w/o type
+        doAnswer {
+            mockAnswer(it, 1, -1, 3, 5)
+        }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* flags */,
+                any() /* executor */, any() /* cancellationSignal */, any() /*callback*/)
+        // Mock DnsResolver.query() w/ type
+        doAnswer {
+            mockAnswer(it, 1, 2, 4, 6)
+        }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* nsType */,
+                anyInt() /* flags */, any() /* executor */, any() /* cancellationSignal */,
+        any() /*callback*/)
+    }
+
+    private fun mockAnswer(
+        it: InvocationOnMock,
+        posHos: Int,
+        posType: Int,
+        posExecutor: Int,
+        posCallback: Int
+    ) {
+        val hostname = it.arguments[posHos] as String
+        val executor = it.arguments[posExecutor] as Executor
+        val callback = it.arguments[posCallback] as DnsResolver.Callback<List<InetAddress>>
+        var type = if (posType != -1) it.arguments[posType] as Int else TYPE_UNSPECIFIED
+        val answer = getAnswer(hostname, type)
+
+        if (!answer?.addresses.isNullOrEmpty()) {
+            Handler(Looper.getMainLooper()).post({ executor.execute({
+                    callback.onAnswer(answer?.addresses, 0); }) })
+        }
+    }
+
+    /** Clears all entries. */
+    fun clearAll() = synchronized(answers) {
+        answers.clear()
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
new file mode 100644
index 0000000..861f45e
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+@file:JvmName("HandlerUtils")
+
+package com.android.testutils
+
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import java.util.concurrent.Executor
+import kotlin.test.fail
+
+/**
+ * Block until the specified Handler or HandlerThread becomes idle, or until timeoutMs has passed.
+ */
+fun HandlerThread.waitForIdle(timeoutMs: Int) = threadHandler.waitForIdle(timeoutMs.toLong())
+fun HandlerThread.waitForIdle(timeoutMs: Long) = threadHandler.waitForIdle(timeoutMs)
+fun Handler.waitForIdle(timeoutMs: Int) = waitForIdle(timeoutMs.toLong())
+fun Handler.waitForIdle(timeoutMs: Long) {
+    val cv = ConditionVariable(false)
+    post(cv::open)
+    if (!cv.block(timeoutMs)) {
+        fail("Handler did not become idle after ${timeoutMs}ms")
+    }
+}
+
+/**
+ * Block until the given Serial Executor becomes idle, or until timeoutMs has passed.
+ */
+fun waitForIdleSerialExecutor(executor: Executor, timeoutMs: Long) {
+    val cv = ConditionVariable()
+    executor.execute(cv::open)
+    if (!cv.block(timeoutMs)) {
+        fail("Executor did not become idle after ${timeoutMs}ms")
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java
new file mode 100644
index 0000000..e84a224
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.net.NetworkStats;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.os.RemoteException;
+
+/**
+ * A shim class that allows {@link TestableNetworkStatsProviderCbBinder} to be built against
+ * different SDK versions.
+ */
+public class NetworkStatsProviderCbStubCompat extends INetworkStatsProviderCallback.Stub {
+    @Override
+    public void notifyStatsUpdated(int token, NetworkStats ifaceStats, NetworkStats uidStats)
+            throws RemoteException {}
+
+    @Override
+    public void notifyAlertReached() throws RemoteException {}
+
+    // Removed in S.
+    public void notifyLimitReached() throws RemoteException {}
+
+    // Added in S.
+    public void notifyWarningOrLimitReached() throws RemoteException {}
+
+    @Override
+    public void unregister() throws RemoteException {}
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java
new file mode 100644
index 0000000..a77aa02
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderStubCompat.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.net.netstats.provider.INetworkStatsProvider;
+
+/**
+ * A shim class that allows {@link TestableNetworkStatsProviderBinder} to be built against
+ * different SDK versions.
+ */
+public class NetworkStatsProviderStubCompat extends INetworkStatsProvider.Stub {
+    @Override
+    public void onRequestStatsUpdate(int token) {}
+
+    // Removed and won't be called in S+.
+    public void onSetLimit(String iface, long quotaBytes) {}
+
+    @Override
+    public void onSetAlert(long bytes) {}
+
+    // Added in S.
+    public void onSetWarningAndLimit(String iface, long warningBytes, long limitBytes) {}
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
new file mode 100644
index 0000000..8324b25
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 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.net.NetworkStats
+import kotlin.test.assertTrue
+
+@JvmOverloads
+fun orderInsensitiveEquals(
+    leftStats: NetworkStats,
+    rightStats: NetworkStats,
+    compareTime: Boolean = false
+): Boolean {
+    if (leftStats == rightStats) return true
+    if (compareTime && leftStats.getElapsedRealtime() != rightStats.getElapsedRealtime()) {
+        return false
+    }
+
+    // While operations such as add/subtract will preserve empty entries. This will make
+    // the result be hard to verify during test. Remove them before comparing since they
+    // are not really affect correctness.
+    // TODO (b/152827872): Remove empty entries after addition/subtraction.
+    val leftTrimmedEmpty = leftStats.removeEmptyEntries()
+    val rightTrimmedEmpty = rightStats.removeEmptyEntries()
+
+    if (leftTrimmedEmpty.size() != rightTrimmedEmpty.size()) return false
+    val left = NetworkStats.Entry()
+    val right = NetworkStats.Entry()
+    // Order insensitive compare.
+    for (i in 0 until leftTrimmedEmpty.size()) {
+        leftTrimmedEmpty.getValues(i, left)
+        val j: Int = rightTrimmedEmpty.findIndexHinted(left.iface, left.uid, left.set, left.tag,
+                left.metered, left.roaming, left.defaultNetwork, i)
+        if (j == -1) return false
+        rightTrimmedEmpty.getValues(j, right)
+        if (left != right) return false
+    }
+    return true
+}
+
+/**
+ * Assert that two {@link NetworkStats} are equals, assuming the order of the records are not
+ * necessarily the same.
+ *
+ * @note {@code elapsedRealtime} is not compared by default, given that in test cases that is not
+ *       usually used.
+ */
+@JvmOverloads
+fun assertNetworkStatsEquals(
+    expected: NetworkStats,
+    actual: NetworkStats,
+    compareTime: Boolean = false
+) {
+    assertTrue(orderInsensitiveEquals(expected, actual, compareTime),
+            "expected: " + expected + " but was: " + actual)
+}
+
+/**
+ * Assert that after being parceled then unparceled, {@link NetworkStats} is equal to the original
+ * object.
+ */
+fun assertParcelingIsLossless(stats: NetworkStats) {
+    assertParcelingIsLossless(stats, { a, b -> orderInsensitiveEquals(a, b) })
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
new file mode 100644
index 0000000..daf29e4
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 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 java.util.function.Predicate
+
+private const val POLL_FREQUENCY_MS = 1000L
+
+/**
+ * A class that can be used to reply to packets from a [TapPacketReader].
+ *
+ * A reply thread will be created to reply to incoming packets asynchronously.
+ * The receiver creates a new read head on the [TapPacketReader], to read packets, so it does not
+ * affect packets obtained through [TapPacketReader.popPacket].
+ *
+ * @param reader a [TapPacketReader] to obtain incoming packets and reply to them.
+ * @param packetFilter A filter to apply to incoming packets.
+ * @param name Name to use for the internal responder thread.
+ */
+abstract class PacketResponder(
+    private val reader: TapPacketReader,
+    private val packetFilter: Predicate<ByteArray>,
+    name: String
+) {
+    private val replyThread = ReplyThread(name)
+
+    protected abstract fun replyToPacket(packet: ByteArray, reader: TapPacketReader)
+
+    /**
+     * Start the [PacketResponder].
+     */
+    fun start() {
+        replyThread.start()
+    }
+
+    /**
+     * Stop the [PacketResponder].
+     *
+     * The responder cannot be used anymore after being stopped.
+     */
+    fun stop() {
+        replyThread.interrupt()
+    }
+
+    private inner class ReplyThread(name: String) : Thread(name) {
+        override fun run() {
+            try {
+                // Create a new ReadHead so other packets polled on the reader are not affected
+                val recvPackets = reader.receivedPackets.newReadHead()
+                while (!isInterrupted) {
+                    recvPackets.poll(POLL_FREQUENCY_MS, packetFilter::test)?.let {
+                        replyToPacket(it, reader)
+                    }
+                }
+            } catch (e: InterruptedException) {
+                // Exit gracefully
+            }
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ParcelUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/ParcelUtils.kt
new file mode 100644
index 0000000..14ed8e9
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ParcelUtils.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+@file:JvmName("ParcelUtils")
+
+package com.android.testutils
+
+import android.os.Parcel
+import android.os.Parcelable
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+/**
+ * Return a new instance of `T` after being parceled then unparceled.
+ */
+fun <T : Parcelable> parcelingRoundTrip(source: T): T {
+    val creator: Parcelable.Creator<T>
+    try {
+        creator = source.javaClass.getField("CREATOR").get(null) as Parcelable.Creator<T>
+    } catch (e: IllegalAccessException) {
+        fail("Missing CREATOR field: " + e.message)
+    } catch (e: NoSuchFieldException) {
+        fail("Missing CREATOR field: " + e.message)
+    }
+
+    var p = Parcel.obtain()
+    source.writeToParcel(p, /* flags */ 0)
+    p.setDataPosition(0)
+    val marshalled = p.marshall()
+    p = Parcel.obtain()
+    p.unmarshall(marshalled, 0, marshalled.size)
+    p.setDataPosition(0)
+    return creator.createFromParcel(p)
+}
+
+/**
+ * Assert that after being parceled then unparceled, `source` is equal to the original
+ * object. If a customized equals function is provided, uses the provided one.
+ */
+@JvmOverloads
+fun <T : Parcelable> assertParcelingIsLossless(
+    source: T,
+    equals: (T, T) -> Boolean = { a, b -> a == b }
+) {
+    val actual = parcelingRoundTrip(source)
+    assertTrue(equals(source, actual), "Expected $source, but was $actual")
+}
+
+@JvmOverloads
+fun <T : Parcelable> assertParcelSane(
+    obj: T,
+    fieldCount: Int,
+    equals: (T, T) -> Boolean = { a, b -> a == b }
+) {
+    assertFieldCountEquals(fieldCount, obj::class.java)
+    assertParcelingIsLossless(obj, equals)
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReader.java b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReader.java
new file mode 100644
index 0000000..b25b9f2
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReader.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2020 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.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.ArrayTrackRecord;
+import com.android.net.module.util.PacketReader;
+
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.function.Predicate;
+
+import kotlin.Lazy;
+import kotlin.LazyKt;
+
+/**
+ * A packet reader that runs on a TAP interface.
+ *
+ * It also implements facilities to reply to received packets.
+ */
+public class TapPacketReader extends PacketReader {
+    private final FileDescriptor mTapFd;
+    private final ArrayTrackRecord<byte[]> mReceivedPackets = new ArrayTrackRecord<>();
+    private final Lazy<ArrayTrackRecord<byte[]>.ReadHead> mReadHead =
+            LazyKt.lazy(mReceivedPackets::newReadHead);
+
+    public TapPacketReader(Handler h, FileDescriptor tapFd, int maxPacketSize) {
+        super(h, maxPacketSize);
+        mTapFd = tapFd;
+    }
+
+
+    /**
+     * Attempt to start the FdEventsReader on its handler thread.
+     *
+     * As opposed to {@link android.net.util.FdEventsReader#start()}, this method will not report
+     * failure to start, so it is only appropriate in tests that will fail later if that happens.
+     */
+    public void startAsyncForTest() {
+        getHandler().post(this::start);
+    }
+
+    @Override
+    protected FileDescriptor createFd() {
+        return mTapFd;
+    }
+
+    @Override
+    protected void handlePacket(byte[] recvbuf, int length) {
+        final byte[] newPacket = Arrays.copyOf(recvbuf, length);
+        if (!mReceivedPackets.add(newPacket)) {
+            throw new AssertionError("More than " + Integer.MAX_VALUE + " packets outstanding!");
+        }
+    }
+
+    /**
+     * @deprecated This method does not actually "pop" (which generally means the last packet).
+     * Use {@link #poll(long)}, which has the same behavior, instead.
+     */
+    @Nullable
+    @Deprecated
+    public byte[] popPacket(long timeoutMs) {
+        return poll(timeoutMs);
+    }
+
+    /**
+     * @deprecated This method does not actually "pop" (which generally means the last packet).
+     * Use {@link #poll(long, Predicate)}, which has the same behavior, instead.
+     */
+    @Nullable
+    @Deprecated
+    public byte[] popPacket(long timeoutMs, @NonNull Predicate<byte[]> filter) {
+        return poll(timeoutMs, filter);
+    }
+
+    /**
+     * Get the next packet that was received on the interface.
+     */
+    @Nullable
+    public byte[] poll(long timeoutMs) {
+        return mReadHead.getValue().poll(timeoutMs, packet -> true);
+    }
+
+    /**
+     * Get the next packet that was received on the interface and matches the specified filter.
+     */
+    @Nullable
+    public byte[] poll(long timeoutMs, @NonNull Predicate<byte[]> filter) {
+        return mReadHead.getValue().poll(timeoutMs, filter::test);
+    }
+
+    /**
+     * Get the {@link ArrayTrackRecord} that records all packets received by the reader since its
+     * creation.
+     */
+    public ArrayTrackRecord<byte[]> getReceivedPackets() {
+        return mReceivedPackets;
+    }
+
+    /*
+     * Send a response on the TAP interface.
+     *
+     * The passed ByteBuffer is flipped after use.
+     *
+     * @param packet The packet to send.
+     * @throws IOException if the interface can't be written to.
+     */
+    public void sendResponse(final ByteBuffer packet) throws IOException {
+        try (FileOutputStream out = new FileOutputStream(mTapFd)) {
+            byte[] packetBytes = new byte[packet.limit()];
+            packet.get(packetBytes);
+            packet.flip();  // So we can reuse it in the future.
+            out.write(packetBytes);
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
new file mode 100644
index 0000000..701666c
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TapPacketReaderRule.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2020 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.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import kotlin.test.assertFalse
+import kotlin.test.fail
+
+private const val HANDLER_TIMEOUT_MS = 10_000L
+
+/**
+ * A [TestRule] that sets up a [TapPacketReader] on a [TestNetworkInterface] for use in the test.
+ *
+ * @param maxPacketSize Maximum size of packets read in the [TapPacketReader] buffer.
+ * @param autoStart Whether to initialize the interface and start the reader automatically for every
+ *                  test. If false, each test must either call start() and stop(), or be annotated
+ *                  with TapPacketReaderTest before using the reader or interface.
+ */
+class TapPacketReaderRule @JvmOverloads constructor(
+    private val maxPacketSize: Int = 1500,
+    private val autoStart: Boolean = true
+) : TestRule {
+    // Use lateinit as the below members can't be initialized in the rule constructor (the
+    // InstrumentationRegistry may not be ready), but from the point of view of test cases using
+    // this rule with autoStart = true, the members are always initialized (in setup/test/teardown):
+    // tests cases should be able use them directly.
+    // lateinit also allows getting good exceptions detailing what went wrong if the members are
+    // referenced before they could be initialized (typically if autoStart is false and the test
+    // does not call start or use @TapPacketReaderTest).
+    lateinit var iface: TestNetworkInterface
+    lateinit var reader: TapPacketReader
+
+    @Volatile
+    private var readerRunning = false
+
+    /**
+     * Indicates that the [TapPacketReaderRule] should initialize its [TestNetworkInterface] and
+     * start the [TapPacketReader] before the test, and tear them down afterwards.
+     *
+     * For use when [TapPacketReaderRule] is created with autoStart = false.
+     */
+    annotation class TapPacketReaderTest
+
+    /**
+     * Initialize the tap interface and start the [TapPacketReader].
+     *
+     * Tests using this method must also call [stop] before exiting.
+     * @param handler Handler to run the reader on. Callers are responsible for safely terminating
+     *                the handler when the test ends. If null, a handler thread managed by the
+     *                rule will be used.
+     */
+    @JvmOverloads
+    fun start(handler: Handler? = null) {
+        if (this::iface.isInitialized) {
+            fail("${TapPacketReaderRule::class.java.simpleName} was already started")
+        }
+
+        val ctx = InstrumentationRegistry.getInstrumentation().context
+        iface = runAsShell(MANAGE_TEST_NETWORKS) {
+            val tnm = ctx.getSystemService(TestNetworkManager::class.java)
+                    ?: fail("Could not obtain the TestNetworkManager")
+            tnm.createTapInterface()
+        }
+        val usedHandler = handler ?: HandlerThread(
+                TapPacketReaderRule::class.java.simpleName).apply { start() }.threadHandler
+        reader = TapPacketReader(usedHandler, iface.fileDescriptor.fileDescriptor, maxPacketSize)
+        reader.startAsyncForTest()
+        readerRunning = true
+    }
+
+    /**
+     * Stop the [TapPacketReader].
+     *
+     * Tests calling [start] must call this method before exiting. If a handler was specified in
+     * [start], all messages on that handler must also be processed after calling this method and
+     * before exiting.
+     *
+     * If [start] was not called, calling this method is a no-op.
+     */
+    fun stop() {
+        // The reader may not be initialized if the test case did not use the rule, even though
+        // other test cases in the same class may be using it (so test classes may call stop in
+        // tearDown even if start is not called for all test cases).
+        if (!this::reader.isInitialized) return
+        reader.handler.post {
+            reader.stop()
+            readerRunning = false
+        }
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return TapReaderStatement(base, description)
+    }
+
+    private inner class TapReaderStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            val shouldStart = autoStart ||
+                    description.getAnnotation(TapPacketReaderTest::class.java) != null
+            if (shouldStart) {
+                start()
+            }
+
+            try {
+                base.evaluate()
+            } finally {
+                if (shouldStart) {
+                    stop()
+                    reader.handler.looper.apply {
+                        quitSafely()
+                        thread.join(HANDLER_TIMEOUT_MS)
+                        assertFalse(thread.isAlive,
+                                "HandlerThread did not exit within $HANDLER_TIMEOUT_MS ms")
+                    }
+                }
+
+                if (this@TapPacketReaderRule::iface.isInitialized) {
+                    iface.fileDescriptor.close()
+                }
+            }
+
+            assertFalse(readerRunning,
+                    "stop() was not called, or the provided handler did not process the stop " +
+                    "message before the test ended. If not using autostart, make sure to call " +
+                    "stop() after the test. If a handler is specified in start(), make sure all " +
+                    "messages are processed after calling stop(), before quitting (for example " +
+                    "by using HandlerThread#quitSafely and HandlerThread#join).")
+        }
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
new file mode 100644
index 0000000..7aae8e3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2020 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.net.Uri
+import com.android.net.module.util.ArrayTrackRecord
+import fi.iki.elonen.NanoHTTPD
+
+/**
+ * A minimal HTTP server running on a random available port.
+ *
+ * @param host The host to listen to, or null to listen on all hosts
+ */
+class TestHttpServer(host: String? = null) : NanoHTTPD(host, 0 /* auto-select the port */) {
+    // Map of URL path -> HTTP response code
+    private val responses = HashMap<Request, Response>()
+
+    /**
+     * A record of all requests received by the server since it was started.
+     */
+    val requestsRecord = ArrayTrackRecord<Request>()
+
+    /**
+     * A request received by the test server.
+     */
+    data class Request(
+        val path: String,
+        val method: Method = Method.GET,
+        val queryParameters: String = ""
+    ) {
+        /**
+         * Returns whether the specified [Uri] matches parameters of this request.
+         */
+        fun matches(uri: Uri) = (uri.path ?: "") == path && (uri.query ?: "") == queryParameters
+    }
+
+    /**
+     * Add a response for GET requests with the path and query parameters of the specified [Uri].
+     */
+    fun addResponse(
+        uri: Uri,
+        statusCode: Response.IStatus,
+        locationHeader: String? = null,
+        content: String = ""
+    ) {
+        addResponse(Request(uri.path
+                ?: "", Method.GET, uri.query ?: ""),
+                statusCode, locationHeader, content)
+    }
+
+    /**
+     * Add a response for the given request.
+     */
+    fun addResponse(
+        request: Request,
+        statusCode: Response.IStatus,
+        locationHeader: String? = null,
+        content: String = ""
+    ) {
+        val response = newFixedLengthResponse(statusCode, "text/plain", content)
+        locationHeader?.let { response.addHeader("Location", it) }
+        responses[request] = response
+    }
+
+    override fun serve(session: IHTTPSession): Response {
+        val request = Request(session.uri
+                ?: "", session.method, session.queryParameterString ?: "")
+        requestsRecord.add(request)
+        // Default response is a 404
+        return responses[request] ?: super.serve(session)
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
new file mode 100644
index 0000000..5b97861
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2020 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.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.os.Binder
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+
+/**
+ * Create a test network based on a TUN interface.
+ *
+ * This method will block until the test network is available. Requires
+ * [android.Manifest.permission.CHANGE_NETWORK_STATE] and
+ * [android.Manifest.permission.MANAGE_TEST_NETWORKS].
+ */
+fun initTestNetwork(context: Context, interfaceAddr: LinkAddress, setupTimeoutMs: Long = 10_000L):
+        TestNetworkTracker {
+    val tnm = context.getSystemService(TestNetworkManager::class.java)
+    val iface = tnm.createTunInterface(arrayOf(interfaceAddr))
+    return TestNetworkTracker(context, iface, tnm, setupTimeoutMs)
+}
+
+/**
+ * Utility class to create and track test networks.
+ *
+ * This class is not thread-safe.
+ */
+class TestNetworkTracker internal constructor(
+    val context: Context,
+    val iface: TestNetworkInterface,
+    tnm: TestNetworkManager,
+    setupTimeoutMs: Long
+) {
+    private val cm = context.getSystemService(ConnectivityManager::class.java)
+    private val binder = Binder()
+
+    private val networkCallback: NetworkCallback
+    val network: Network
+
+    init {
+        val networkFuture = CompletableFuture<Network>()
+        val networkRequest = NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                // Test networks do not have NOT_VPN or TRUSTED capabilities by default
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(iface.interfaceName))
+                .build()
+        networkCallback = object : NetworkCallback() {
+            override fun onAvailable(network: Network) {
+                networkFuture.complete(network)
+            }
+        }
+        cm.requestNetwork(networkRequest, networkCallback)
+
+        try {
+            tnm.setupTestNetwork(iface.interfaceName, binder)
+            network = networkFuture.get(setupTimeoutMs, TimeUnit.MILLISECONDS)
+        } catch (e: Throwable) {
+            teardown()
+            throw e
+        }
+    }
+
+    fun teardown() {
+        cm.unregisterNetworkCallback(networkCallback)
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
new file mode 100644
index 0000000..f557f18
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+@file:JvmName("TestPermissionUtil")
+
+package com.android.testutils
+
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ExceptionUtils.ThrowingRunnable
+import com.android.testutils.ExceptionUtils.ThrowingSupplier
+
+/**
+ * Run the specified [task] with the specified [permissions] obtained through shell
+ * permission identity.
+ *
+ * Passing in an empty list of permissions can grant all shell permissions, but this is
+ * discouraged as it also causes the process to temporarily lose non-shell permissions.
+ */
+fun <T> runAsShell(vararg permissions: String, task: () -> T): T {
+    val autom = InstrumentationRegistry.getInstrumentation().uiAutomation
+    autom.adoptShellPermissionIdentity(*permissions)
+    try {
+        return task()
+    } finally {
+        autom.dropShellPermissionIdentity()
+    }
+}
+
+/**
+ * Convenience overload of [runAsShell] that uses a [ThrowingSupplier] for Java callers, when
+ * only one/two/three permissions are needed.
+ */
+@JvmOverloads
+fun <T> runAsShell(
+    perm1: String,
+    perm2: String = "",
+    perm3: String = "",
+    supplier: ThrowingSupplier<T>
+): T = runAsShell(*getNonEmptyVarargs(perm1, perm2, perm3)) { supplier.get() }
+
+/**
+ * Convenience overload of [runAsShell] that uses a [ThrowingRunnable] for Java callers, when
+ * only one/two/three permissions are needed.
+ */
+@JvmOverloads
+fun runAsShell(
+    perm1: String,
+    perm2: String = "",
+    perm3: String = "",
+    runnable: ThrowingRunnable
+): Unit = runAsShell(*getNonEmptyVarargs(perm1, perm2, perm3)) { runnable.run() }
+
+/**
+ * Get an array containing the first consecutive non-empty arguments out of three arguments.
+ *
+ * The first argument is assumed to be non-empty.
+ */
+private fun getNonEmptyVarargs(arg1: String, arg2: String, arg3: String): Array<String> {
+    return when {
+        arg2 == "" -> arrayOf(arg1)
+        arg3 == "" -> arrayOf(arg1, arg2)
+        else -> arrayOf(arg1, arg2, arg3)
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
new file mode 100644
index 0000000..1b3d0f6
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2019 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.net.ConnectivityManager.NetworkCallback
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatusInt
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Losing
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
+import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import kotlin.reflect.KClass
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+object NULL_NETWORK : Network(-1)
+object ANY_NETWORK : Network(-2)
+
+private val Int.capabilityName get() = NetworkCapabilities.capabilityNameOf(this)
+
+open class RecorderCallback private constructor(
+    private val backingRecord: ArrayTrackRecord<CallbackEntry>
+) : NetworkCallback() {
+    public constructor() : this(ArrayTrackRecord())
+    protected constructor(src: RecorderCallback?): this(src?.backingRecord ?: ArrayTrackRecord())
+
+    private val TAG = this::class.simpleName
+
+    sealed class CallbackEntry {
+        // To get equals(), hashcode(), componentN() etc for free, the child classes of
+        // this class are data classes. But while data classes can inherit from other classes,
+        // they may only have visible members in the constructors, so they couldn't declare
+        // a constructor with a non-val arg to pass to CallbackEntry. Instead, force all
+        // subclasses to implement a `network' property, which can be done in a data class
+        // constructor by specifying override.
+        abstract val network: Network
+
+        data class Available(override val network: Network) : CallbackEntry()
+        data class CapabilitiesChanged(
+            override val network: Network,
+            val caps: NetworkCapabilities
+        ) : CallbackEntry()
+        data class LinkPropertiesChanged(
+            override val network: Network,
+            val lp: LinkProperties
+        ) : CallbackEntry()
+        data class Suspended(override val network: Network) : CallbackEntry()
+        data class Resumed(override val network: Network) : CallbackEntry()
+        data class Losing(override val network: Network, val maxMsToLive: Int) : CallbackEntry()
+        data class Lost(override val network: Network) : CallbackEntry()
+        data class Unavailable private constructor(
+            override val network: Network
+        ) : CallbackEntry() {
+            constructor() : this(NULL_NETWORK)
+        }
+        data class BlockedStatus(
+            override val network: Network,
+            val blocked: Boolean
+        ) : CallbackEntry()
+        data class BlockedStatusInt(
+            override val network: Network,
+            val blocked: Int
+        ) : CallbackEntry()
+        // Convenience constants for expecting a type
+        companion object {
+            @JvmField
+            val AVAILABLE = Available::class
+            @JvmField
+            val NETWORK_CAPS_UPDATED = CapabilitiesChanged::class
+            @JvmField
+            val LINK_PROPERTIES_CHANGED = LinkPropertiesChanged::class
+            @JvmField
+            val SUSPENDED = Suspended::class
+            @JvmField
+            val RESUMED = Resumed::class
+            @JvmField
+            val LOSING = Losing::class
+            @JvmField
+            val LOST = Lost::class
+            @JvmField
+            val UNAVAILABLE = Unavailable::class
+            @JvmField
+            val BLOCKED_STATUS = BlockedStatus::class
+            @JvmField
+            val BLOCKED_STATUS_INT = BlockedStatusInt::class
+        }
+    }
+
+    val history = backingRecord.newReadHead()
+    val mark get() = history.mark
+
+    override fun onAvailable(network: Network) {
+        history.add(Available(network))
+    }
+
+    // PreCheck is not used in the tests today. For backward compatibility with existing tests that
+    // expect the callbacks not to record this, do not listen to PreCheck here.
+
+    override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
+        Log.d(TAG, "onCapabilitiesChanged $network $caps")
+        history.add(CapabilitiesChanged(network, caps))
+    }
+
+    override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) {
+        Log.d(TAG, "onLinkPropertiesChanged $network $lp")
+        history.add(LinkPropertiesChanged(network, lp))
+    }
+
+    override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
+        Log.d(TAG, "onBlockedStatusChanged $network $blocked")
+        history.add(BlockedStatus(network, blocked))
+    }
+
+    // Cannot do:
+    // fun onBlockedStatusChanged(network: Network, blocked: Int) {
+    // because on S, that needs to be "override fun", and on R, that cannot be "override fun".
+    override fun onNetworkSuspended(network: Network) {
+        Log.d(TAG, "onNetworkSuspended $network $network")
+        history.add(Suspended(network))
+    }
+
+    override fun onNetworkResumed(network: Network) {
+        Log.d(TAG, "$network onNetworkResumed $network")
+        history.add(Resumed(network))
+    }
+
+    override fun onLosing(network: Network, maxMsToLive: Int) {
+        Log.d(TAG, "onLosing $network $maxMsToLive")
+        history.add(Losing(network, maxMsToLive))
+    }
+
+    override fun onLost(network: Network) {
+        Log.d(TAG, "onLost $network")
+        history.add(Lost(network))
+    }
+
+    override fun onUnavailable() {
+        Log.d(TAG, "onUnavailable")
+        history.add(Unavailable())
+    }
+}
+
+private const val DEFAULT_TIMEOUT = 200L // ms
+
+open class TestableNetworkCallback private constructor(
+    src: TestableNetworkCallback?,
+    val defaultTimeoutMs: Long = DEFAULT_TIMEOUT
+) : RecorderCallback(src) {
+    @JvmOverloads
+    constructor(timeoutMs: Long = DEFAULT_TIMEOUT): this(null, timeoutMs)
+
+    fun createLinkedCopy() = TestableNetworkCallback(this, defaultTimeoutMs)
+
+    // The last available network, or null if any network was lost since the last call to
+    // onAvailable. TODO : fix this by fixing the tests that rely on this behavior
+    val lastAvailableNetwork: Network?
+        get() = when (val it = history.lastOrNull { it is Available || it is Lost }) {
+            is Available -> it.network
+            else -> null
+        }
+
+    fun pollForNextCallback(timeoutMs: Long = defaultTimeoutMs): CallbackEntry {
+        return history.poll(timeoutMs) ?: fail("Did not receive callback after ${timeoutMs}ms")
+    }
+
+    // Make open for use in ConnectivityServiceTest which is the only one knowing its handlers.
+    // TODO : remove the necessity to overload this, remove the open qualifier, and give a
+    // default argument to assertNoCallback instead, possibly with @JvmOverloads if necessary.
+    open fun assertNoCallback() = assertNoCallback(defaultTimeoutMs)
+
+    fun assertNoCallback(timeoutMs: Long) {
+        val cb = history.poll(timeoutMs)
+        if (null != cb) fail("Expected no callback but got $cb")
+    }
+
+    // Expects a callback of the specified type on the specified network within the timeout.
+    // If no callback arrives, or a different callback arrives, fail. Returns the callback.
+    inline fun <reified T : CallbackEntry> expectCallback(
+        network: Network = ANY_NETWORK,
+        timeoutMs: Long = defaultTimeoutMs
+    ): T = pollForNextCallback(timeoutMs).let {
+        if (it !is T || (ANY_NETWORK !== network && it.network != network)) {
+            fail("Unexpected callback : $it, expected ${T::class} with Network[$network]")
+        } else {
+            it
+        }
+    }
+
+    // Expects a callback of the specified type matching the predicate within the timeout.
+    // Any callback that doesn't match the predicate will be skipped. Fails only if
+    // no matching callback is received within the timeout.
+    inline fun <reified T : CallbackEntry> eventuallyExpect(
+        timeoutMs: Long = defaultTimeoutMs,
+        from: Int = mark,
+        crossinline predicate: (T) -> Boolean = { true }
+    ): T = eventuallyExpectOrNull(timeoutMs, from, predicate).also {
+        assertNotNull(it, "Callback ${T::class} not received within ${timeoutMs}ms")
+    } as T
+
+    fun <T : CallbackEntry> eventuallyExpect(
+        type: KClass<T>,
+        timeoutMs: Long = defaultTimeoutMs,
+        predicate: (T: CallbackEntry) -> Boolean = { true }
+    ) = history.poll(timeoutMs) { type.java.isInstance(it) && predicate(it) }.also {
+        assertNotNull(it, "Callback ${type.java} not received within ${timeoutMs}ms")
+    } as T
+
+    // TODO (b/157405399) straighten and unify the method names
+    inline fun <reified T : CallbackEntry> eventuallyExpectOrNull(
+        timeoutMs: Long = defaultTimeoutMs,
+        from: Int = mark,
+        crossinline predicate: (T) -> Boolean = { true }
+    ) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T?
+
+    fun expectCallbackThat(
+        timeoutMs: Long = defaultTimeoutMs,
+        valid: (CallbackEntry) -> Boolean
+    ) = pollForNextCallback(timeoutMs).also { assertTrue(valid(it), "Unexpected callback : $it") }
+
+    fun expectCapabilitiesThat(
+        net: Network,
+        tmt: Long = defaultTimeoutMs,
+        valid: (NetworkCapabilities) -> Boolean
+    ): CapabilitiesChanged {
+        return expectCallback<CapabilitiesChanged>(net, tmt).also {
+            assertTrue(valid(it.caps), "Capabilities don't match expectations ${it.caps}")
+        }
+    }
+
+    fun expectLinkPropertiesThat(
+        net: Network,
+        tmt: Long = defaultTimeoutMs,
+        valid: (LinkProperties) -> Boolean
+    ): LinkPropertiesChanged {
+        return expectCallback<LinkPropertiesChanged>(net, tmt).also {
+            assertTrue(valid(it.lp), "LinkProperties don't match expectations ${it.lp}")
+        }
+    }
+
+    // Expects onAvailable and the callbacks that follow it. These are:
+    // - onSuspended, iff the network was suspended when the callbacks fire.
+    // - onCapabilitiesChanged.
+    // - onLinkPropertiesChanged.
+    // - onBlockedStatusChanged.
+    //
+    // @param network the network to expect the callbacks on.
+    // @param suspended whether to expect a SUSPENDED callback.
+    // @param validated the expected value of the VALIDATED capability in the
+    //        onCapabilitiesChanged callback.
+    // @param tmt how long to wait for the callbacks.
+    fun expectAvailableCallbacks(
+        net: Network,
+        suspended: Boolean = false,
+        validated: Boolean = true,
+        blocked: Boolean = false,
+        tmt: Long = defaultTimeoutMs
+    ) {
+        expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+        expectBlockedStatusCallback(blocked, net, tmt)
+    }
+
+    fun expectAvailableCallbacks(
+        net: Network,
+        suspended: Boolean,
+        validated: Boolean,
+        blockedStatus: Int,
+        tmt: Long
+    ) {
+        expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+        expectBlockedStatusCallback(blockedStatus, net)
+    }
+
+    private fun expectAvailableCallbacksCommon(
+        net: Network,
+        suspended: Boolean,
+        validated: Boolean,
+        tmt: Long
+    ) {
+        expectCallback<Available>(net, tmt)
+        if (suspended) {
+            expectCallback<Suspended>(net, tmt)
+        }
+        expectCapabilitiesThat(net, tmt) { validated == it.hasCapability(NET_CAPABILITY_VALIDATED) }
+        expectCallback<LinkPropertiesChanged>(net, tmt)
+    }
+
+    // Backward compatibility for existing Java code. Use named arguments instead and remove all
+    // these when there is no user left.
+    fun expectAvailableAndSuspendedCallbacks(
+        net: Network,
+        validated: Boolean,
+        tmt: Long = defaultTimeoutMs
+    ) = expectAvailableCallbacks(net, suspended = true, validated = validated, tmt = tmt)
+
+    fun expectBlockedStatusCallback(blocked: Boolean, net: Network, tmt: Long = defaultTimeoutMs) {
+        expectCallback<BlockedStatus>(net, tmt).also {
+            assertEquals(it.blocked, blocked, "Unexpected blocked status ${it.blocked}")
+        }
+    }
+
+    fun expectBlockedStatusCallback(blocked: Int, net: Network, tmt: Long = defaultTimeoutMs) {
+        expectCallback<BlockedStatusInt>(net, tmt).also {
+            assertEquals(it.blocked, blocked, "Unexpected blocked status ${it.blocked}")
+        }
+    }
+
+    // Expects the available callbacks (where the onCapabilitiesChanged must contain the
+    // VALIDATED capability), plus another onCapabilitiesChanged which is identical to the
+    // one we just sent.
+    // TODO: this is likely a bug. Fix it and remove this method.
+    fun expectAvailableDoubleValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
+        val mark = history.mark
+        expectAvailableCallbacks(net, tmt = tmt)
+        val firstCaps = history.poll(tmt, mark) { it is CapabilitiesChanged }
+        assertEquals(firstCaps, expectCallback<CapabilitiesChanged>(net, tmt))
+    }
+
+    // Expects the available callbacks where the onCapabilitiesChanged must not have validated,
+    // then expects another onCapabilitiesChanged that has the validated bit set. This is used
+    // when a network connects and satisfies a callback, and then immediately validates.
+    fun expectAvailableThenValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
+        expectAvailableCallbacks(net, validated = false, tmt = tmt)
+        expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+    }
+
+    fun expectAvailableThenValidatedCallbacks(
+        net: Network,
+        blockedStatus: Int,
+        tmt: Long = defaultTimeoutMs
+    ) {
+        expectAvailableCallbacks(net, validated = false, suspended = false,
+                blockedStatus = blockedStatus, tmt = tmt)
+        expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+    }
+
+    // Temporary Java compat measure : have MockNetworkAgent implement this so that all existing
+    // calls with networkAgent can be routed through here without moving MockNetworkAgent.
+    // TODO: clean this up, remove this method.
+    interface HasNetwork {
+        val network: Network
+    }
+
+    @JvmOverloads
+    open fun <T : CallbackEntry> expectCallback(
+        type: KClass<T>,
+        n: Network?,
+        timeoutMs: Long = defaultTimeoutMs
+    ) = pollForNextCallback(timeoutMs).also {
+        val network = n ?: NULL_NETWORK
+        // TODO : remove this .java access if the tests ever use kotlin-reflect. At the time of
+        // this writing this would be the only use of this library in the tests.
+        assertTrue(type.java.isInstance(it) && it.network == network,
+                "Unexpected callback : $it, expected ${type.java} with Network[$network]")
+    } as T
+
+    @JvmOverloads
+    open fun <T : CallbackEntry> expectCallback(
+        type: KClass<T>,
+        n: HasNetwork?,
+        timeoutMs: Long = defaultTimeoutMs
+    ) = expectCallback(type, n?.network, timeoutMs)
+
+    fun expectAvailableCallbacks(
+        n: HasNetwork,
+        suspended: Boolean,
+        validated: Boolean,
+        blocked: Boolean,
+        timeoutMs: Long
+    ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, timeoutMs)
+
+    fun expectAvailableAndSuspendedCallbacks(n: HasNetwork, expectValidated: Boolean) {
+        expectAvailableAndSuspendedCallbacks(n.network, expectValidated)
+    }
+
+    fun expectAvailableCallbacksValidated(n: HasNetwork) {
+        expectAvailableCallbacks(n.network)
+    }
+
+    fun expectAvailableCallbacksValidatedAndBlocked(n: HasNetwork) {
+        expectAvailableCallbacks(n.network, blocked = true)
+    }
+
+    fun expectAvailableCallbacksUnvalidated(n: HasNetwork) {
+        expectAvailableCallbacks(n.network, validated = false)
+    }
+
+    fun expectAvailableCallbacksUnvalidatedAndBlocked(n: HasNetwork) {
+        expectAvailableCallbacks(n.network, validated = false, blocked = true)
+    }
+
+    fun expectAvailableDoubleValidatedCallbacks(n: HasNetwork) {
+        expectAvailableDoubleValidatedCallbacks(n.network, defaultTimeoutMs)
+    }
+
+    fun expectAvailableThenValidatedCallbacks(n: HasNetwork) {
+        expectAvailableThenValidatedCallbacks(n.network, defaultTimeoutMs)
+    }
+
+    @JvmOverloads
+    fun expectLinkPropertiesThat(
+        n: HasNetwork,
+        tmt: Long = defaultTimeoutMs,
+        valid: (LinkProperties) -> Boolean
+    ) = expectLinkPropertiesThat(n.network, tmt, valid)
+
+    @JvmOverloads
+    fun expectCapabilitiesThat(
+        n: HasNetwork,
+        tmt: Long = defaultTimeoutMs,
+        valid: (NetworkCapabilities) -> Boolean
+    ) = expectCapabilitiesThat(n.network, tmt, valid)
+
+    @JvmOverloads
+    fun expectCapabilitiesWith(
+        capability: Int,
+        n: HasNetwork,
+        timeoutMs: Long = defaultTimeoutMs
+    ): NetworkCapabilities {
+        return expectCapabilitiesThat(n.network, timeoutMs) { it.hasCapability(capability) }.caps
+    }
+
+    @JvmOverloads
+    fun expectCapabilitiesWithout(
+        capability: Int,
+        n: HasNetwork,
+        timeoutMs: Long = defaultTimeoutMs
+    ): NetworkCapabilities {
+        return expectCapabilitiesThat(n.network, timeoutMs) { !it.hasCapability(capability) }.caps
+    }
+
+    fun expectBlockedStatusCallback(expectBlocked: Boolean, n: HasNetwork) {
+        expectBlockedStatusCallback(expectBlocked, n.network, defaultTimeoutMs)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
new file mode 100644
index 0000000..d5c3a2a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2020 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.net.netstats.provider.NetworkStatsProvider
+import com.android.net.module.util.ArrayTrackRecord
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 200L
+
+open class TestableNetworkStatsProvider(
+    val defaultTimeoutMs: Long = DEFAULT_TIMEOUT_MS
+) : NetworkStatsProvider() {
+    sealed class CallbackType {
+        data class OnRequestStatsUpdate(val token: Int) : CallbackType()
+        data class OnSetLimit(val iface: String?, val quotaBytes: Long) : CallbackType()
+        data class OnSetAlert(val quotaBytes: Long) : CallbackType()
+    }
+
+    val history = ArrayTrackRecord<CallbackType>().newReadHead()
+    // See ReadHead#mark
+    val mark get() = history.mark
+
+    override fun onRequestStatsUpdate(token: Int) {
+        history.add(CallbackType.OnRequestStatsUpdate(token))
+    }
+
+    override fun onSetLimit(iface: String, quotaBytes: Long) {
+        history.add(CallbackType.OnSetLimit(iface, quotaBytes))
+    }
+
+    override fun onSetAlert(quotaBytes: Long) {
+        history.add(CallbackType.OnSetAlert(quotaBytes))
+    }
+
+    fun expectOnRequestStatsUpdate(token: Int, timeout: Long = defaultTimeoutMs) {
+        assertEquals(CallbackType.OnRequestStatsUpdate(token), history.poll(timeout))
+    }
+
+    fun expectOnSetLimit(iface: String?, quotaBytes: Long, timeout: Long = defaultTimeoutMs) {
+        assertEquals(CallbackType.OnSetLimit(iface, quotaBytes), history.poll(timeout))
+    }
+
+    fun expectOnSetAlert(quotaBytes: Long, timeout: Long = defaultTimeoutMs) {
+        assertEquals(CallbackType.OnSetAlert(quotaBytes), history.poll(timeout))
+    }
+
+    fun pollForNextCallback(timeout: Long = defaultTimeoutMs) =
+        history.poll(timeout) ?: fail("Did not receive callback after ${timeout}ms")
+
+    inline fun <reified T : CallbackType> expectCallback(
+        timeout: Long = defaultTimeoutMs,
+        predicate: (T) -> Boolean = { true }
+    ): T {
+        return pollForNextCallback(timeout).also { assertTrue(it is T && predicate(it)) } as T
+    }
+
+    // Expects a callback of the specified type matching the predicate within the timeout.
+    // Any callback that doesn't match the predicate will be skipped. Fails only if
+    // no matching callback is received within the timeout.
+    // TODO : factorize the code for this with the identical call in TestableNetworkCallback.
+    // There should be a common superclass doing this generically.
+    // TODO : have a better error message to have this fail. Right now the failure when no
+    // matching callback arrives comes from the casting to a non-nullable T.
+    // TODO : in fact, completely removing this method and have clients use
+    // history.poll(timeout, index, predicate) directly might be simpler.
+    inline fun <reified T : CallbackType> eventuallyExpect(
+        timeoutMs: Long = defaultTimeoutMs,
+        from: Int = mark,
+        crossinline predicate: (T) -> Boolean = { true }
+    ) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T
+
+    fun drainCallbacks() {
+        history.mark = history.size
+    }
+
+    @JvmOverloads
+    fun assertNoCallback(timeout: Long = defaultTimeoutMs) {
+        val cb = history.poll(timeout)
+        cb?.let { fail("Expected no callback but got $cb") }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
new file mode 100644
index 0000000..643346b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderBinder.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 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 com.android.net.module.util.ArrayTrackRecord
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 200L
+
+open class TestableNetworkStatsProviderBinder : NetworkStatsProviderStubCompat() {
+    sealed class CallbackType {
+        data class OnRequestStatsUpdate(val token: Int) : CallbackType()
+        data class OnSetAlert(val quotaBytes: Long) : CallbackType()
+        data class OnSetWarningAndLimit(
+            val iface: String,
+            val warningBytes: Long,
+            val limitBytes: Long
+        ) : CallbackType()
+    }
+
+    private val history = ArrayTrackRecord<CallbackType>().ReadHead()
+
+    override fun onRequestStatsUpdate(token: Int) {
+        history.add(CallbackType.OnRequestStatsUpdate(token))
+    }
+
+    override fun onSetAlert(quotaBytes: Long) {
+        history.add(CallbackType.OnSetAlert(quotaBytes))
+    }
+
+    override fun onSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+        history.add(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes))
+    }
+
+    fun expectOnRequestStatsUpdate(token: Int) {
+        assertEquals(CallbackType.OnRequestStatsUpdate(token), history.poll(DEFAULT_TIMEOUT_MS))
+    }
+
+    fun expectOnSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+        assertEquals(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes),
+                history.poll(DEFAULT_TIMEOUT_MS))
+    }
+
+    fun expectOnSetAlert(quotaBytes: Long) {
+        assertEquals(CallbackType.OnSetAlert(quotaBytes), history.poll(DEFAULT_TIMEOUT_MS))
+    }
+
+    @JvmOverloads
+    fun assertNoCallback(timeout: Long = DEFAULT_TIMEOUT_MS) {
+        val cb = history.poll(timeout)
+        cb?.let { fail("Expected no callback but got $cb") }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
new file mode 100644
index 0000000..f15f610
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020 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.net.NetworkStats
+import com.android.net.module.util.ArrayTrackRecord
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 3000L
+
+open class TestableNetworkStatsProviderCbBinder : NetworkStatsProviderCbStubCompat() {
+    sealed class CallbackType {
+        data class NotifyStatsUpdated(
+            val token: Int,
+            val ifaceStats: NetworkStats,
+            val uidStats: NetworkStats
+        ) : CallbackType()
+        object NotifyWarningOrLimitReached : CallbackType()
+        object NotifyAlertReached : CallbackType()
+        object Unregister : CallbackType()
+    }
+
+    private val history = ArrayTrackRecord<CallbackType>().ReadHead()
+
+    override fun notifyStatsUpdated(token: Int, ifaceStats: NetworkStats, uidStats: NetworkStats) {
+        history.add(CallbackType.NotifyStatsUpdated(token, ifaceStats, uidStats))
+    }
+
+    override fun notifyWarningOrLimitReached() {
+        history.add(CallbackType.NotifyWarningOrLimitReached)
+    }
+
+    override fun notifyAlertReached() {
+        history.add(CallbackType.NotifyAlertReached)
+    }
+
+    override fun unregister() {
+        history.add(CallbackType.Unregister)
+    }
+
+    fun expectNotifyStatsUpdated() {
+        val event = history.poll(DEFAULT_TIMEOUT_MS)
+        assertTrue(event is CallbackType.NotifyStatsUpdated)
+    }
+
+    fun expectNotifyStatsUpdated(ifaceStats: NetworkStats, uidStats: NetworkStats) {
+        val event = history.poll(DEFAULT_TIMEOUT_MS)!!
+        if (event !is CallbackType.NotifyStatsUpdated) {
+            throw Exception("Expected NotifyStatsUpdated callback, but got ${event::class}")
+        }
+        // TODO: verify token.
+        assertNetworkStatsEquals(ifaceStats, event.ifaceStats)
+        assertNetworkStatsEquals(uidStats, event.uidStats)
+    }
+
+    fun expectNotifyWarningOrLimitReached() =
+            assertEquals(CallbackType.NotifyWarningOrLimitReached, history.poll(DEFAULT_TIMEOUT_MS))
+
+    fun expectNotifyAlertReached() =
+            assertEquals(CallbackType.NotifyAlertReached, history.poll(DEFAULT_TIMEOUT_MS))
+
+    // Assert there is no callback in current queue.
+    fun assertNoCallback() {
+        val cb = history.poll(0)
+        cb?.let { fail("Expected no callback but got $cb") }
+    }
+}
diff --git a/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt b/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt
new file mode 100644
index 0000000..efd77d1
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2019 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.net.module.util
+
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.locks.Condition
+import java.util.concurrent.locks.ReentrantLock
+import java.util.concurrent.locks.StampedLock
+import kotlin.concurrent.withLock
+
+/**
+ * A List that additionally offers the ability to append via the add() method, and to retrieve
+ * an element by its index optionally waiting for it to become available.
+ */
+interface TrackRecord<E> : List<E> {
+    /**
+     * Adds an element to this queue, waking up threads waiting for one. Returns true, as
+     * per the contract for List.
+     */
+    fun add(e: E): Boolean
+
+    /**
+     * Returns the first element after {@param pos}, possibly blocking until one is available, or
+     * null if no such element can be found within the timeout.
+     * If a predicate is given, only elements matching the predicate are returned.
+     *
+     * @param timeoutMs how long, in milliseconds, to wait at most (best effort approximation).
+     * @param pos the position at which to start polling.
+     * @param predicate an optional predicate to filter elements to be returned.
+     * @return an element matching the predicate, or null if timeout.
+     */
+    fun poll(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean = { true }): E?
+}
+
+/**
+ * A thread-safe implementation of TrackRecord that is backed by an ArrayList.
+ *
+ * This class also supports the creation of a read-head for easier single-thread access.
+ * Refer to the documentation of {@link ArrayTrackRecord.ReadHead}.
+ */
+class ArrayTrackRecord<E> : TrackRecord<E> {
+    private val lock = ReentrantLock()
+    private val condition = lock.newCondition()
+    // Backing store. This stores the elements in this ArrayTrackRecord.
+    private val elements = ArrayList<E>()
+
+    // The list iterator for RecordingQueue iterates over a snapshot of the collection at the
+    // time the operator is created. Because TrackRecord is only ever mutated by appending,
+    // that makes this iterator thread-safe as it sees an effectively immutable List.
+    class ArrayTrackRecordIterator<E>(
+        private val list: ArrayList<E>,
+        start: Int,
+        private val end: Int
+    ) : ListIterator<E> {
+        var index = start
+        override fun hasNext() = index < end
+        override fun next() = list[index++]
+        override fun hasPrevious() = index > 0
+        override fun nextIndex() = index + 1
+        override fun previous() = list[--index]
+        override fun previousIndex() = index - 1
+    }
+
+    // List<E> implementation
+    override val size get() = lock.withLock { elements.size }
+    override fun contains(element: E) = lock.withLock { elements.contains(element) }
+    override fun containsAll(elements: Collection<E>) = lock.withLock {
+        this.elements.containsAll(elements)
+    }
+    override operator fun get(index: Int) = lock.withLock { elements[index] }
+    override fun indexOf(element: E): Int = lock.withLock { elements.indexOf(element) }
+    override fun lastIndexOf(element: E): Int = lock.withLock { elements.lastIndexOf(element) }
+    override fun isEmpty() = lock.withLock { elements.isEmpty() }
+    override fun listIterator(index: Int) = ArrayTrackRecordIterator(elements, index, size)
+    override fun listIterator() = listIterator(0)
+    override fun iterator() = listIterator()
+    override fun subList(fromIndex: Int, toIndex: Int): List<E> = lock.withLock {
+        elements.subList(fromIndex, toIndex)
+    }
+
+    // TrackRecord<E> implementation
+    override fun add(e: E): Boolean {
+        lock.withLock {
+            elements.add(e)
+            condition.signalAll()
+        }
+        return true
+    }
+    override fun poll(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean) = lock.withLock {
+        elements.getOrNull(pollForIndexReadLocked(timeoutMs, pos, predicate))
+    }
+
+    // For convenience
+    fun getOrNull(pos: Int, predicate: (E) -> Boolean) = lock.withLock {
+        if (pos < 0 || pos > size) null else elements.subList(pos, size).find(predicate)
+    }
+
+    // Returns the index of the next element whose position is >= pos matching the predicate, if
+    // necessary waiting until such a time that such an element is available, with a timeout.
+    // If no such element is found within the timeout -1 is returned.
+    private fun pollForIndexReadLocked(timeoutMs: Long, pos: Int, predicate: (E) -> Boolean): Int {
+        val deadline = System.currentTimeMillis() + timeoutMs
+        var index = pos
+        do {
+            while (index < elements.size) {
+                if (predicate(elements[index])) return index
+                ++index
+            }
+        } while (condition.await(deadline - System.currentTimeMillis()))
+        return -1
+    }
+
+    /**
+     * Returns a ReadHead over this ArrayTrackRecord. The returned ReadHead is tied to the
+     * current thread.
+     */
+    fun newReadHead() = ReadHead()
+
+    /**
+     * ReadHead is an object that helps users of ArrayTrackRecord keep track of how far
+     * it has read this far in the ArrayTrackRecord. A ReadHead is always associated with
+     * a single instance of ArrayTrackRecord. Multiple ReadHeads can be created and used
+     * on the same instance of ArrayTrackRecord concurrently, and the ArrayTrackRecord
+     * instance can also be used concurrently. ReadHead maintains the current index that is
+     * the next to be read, and calls this the "mark".
+     *
+     * In a ReadHead, {@link poll(Long, (E) -> Boolean)} works similarly to a LinkedBlockingQueue.
+     * It can be called repeatedly and will return the elements as they arrive.
+     *
+     * Intended usage looks something like this :
+     * val TrackRecord<MyObject> record = ArrayTrackRecord().newReadHead()
+     * Thread().start {
+     *   // do stuff
+     *   record.add(something)
+     *   // do stuff
+     * }
+     *
+     * val obj1 = record.poll(timeout)
+     * // do something with obj1
+     * val obj2 = record.poll(timeout)
+     * // do something with obj2
+     *
+     * The point is that the caller does not have to track the mark like it would have to if
+     * it was using ArrayTrackRecord directly.
+     *
+     * Thread safety :
+     * A ReadHead delegates all TrackRecord methods to its associated ArrayTrackRecord, and
+     * inherits its thread-safe properties for all the TrackRecord methods.
+     *
+     * Poll() operates under its own set of rules that only allow execution on multiple threads
+     * within constrained boundaries, and never concurrently or pseudo-concurrently. This is
+     * because concurrent calls to poll() fundamentally do not make sense. poll() will move
+     * the mark according to what events remained to be read by this read head, and therefore
+     * if multiple threads were calling poll() concurrently on the same ReadHead, what
+     * happens to the mark and the return values could not be useful because there is no way to
+     * provide either a guarantee not to skip objects nor a guarantee about the mark position at
+     * the exit of poll(). This is even more true in the presence of a predicate to filter
+     * returned elements, because one thread might be filtering out the events the other is
+     * interested in. For this reason, this class will fail-fast if any concurrent access is
+     * detected with ConcurrentAccessException.
+     * It is possible to use poll() on different threads as long as the following can be
+     * guaranteed : one thread must call poll() for the last time, then execute a write barrier,
+     * then the other thread must execute a read barrier before calling poll() for the first time.
+     * This allows in particular to call poll in @Before and @After methods in JUnit unit tests,
+     * because JUnit will enforce those barriers by creating the testing thread after executing
+     * @Before and joining the thread after executing @After.
+     *
+     * peek() can be used by multiple threads concurrently, but only if no thread is calling
+     * poll() outside of the boundaries above. For simplicity, it can be considered that peek()
+     * is safe to call only when poll() is safe to call.
+     *
+     * Polling concurrently from the same ArrayTrackRecord is supported by creating multiple
+     * ReadHeads on the same instance of ArrayTrackRecord (or of course by using ArrayTrackRecord
+     * directly). Each ReadHead is then guaranteed to see all events always and
+     * guarantees are made on the value of the mark upon return. {@see poll(Long, (E) -> Boolean)}
+     * for details. Be careful to create each ReadHead on the thread it is meant to be used on, or
+     * to have a clear synchronization point between creation and use.
+     *
+     * Users of a ReadHead can ask for the current position of the mark at any time, on a thread
+     * where it's safe to call peek(). This mark can be used later to replay the history of events
+     * either on this ReadHead, on the associated ArrayTrackRecord or on another ReadHead
+     * associated with the same ArrayTrackRecord. It might look like this in the reader thread :
+     *
+     * val markAtStart = record.mark
+     * // Start processing interesting events
+     * while (val element = record.poll(timeout) { it.isInteresting() }) {
+     *   // Do something with element
+     * }
+     * // Look for stuff that happened while searching for interesting events
+     * val firstElementReceived = record.getOrNull(markAtStart)
+     * val firstSpecialElement = record.getOrNull(markAtStart) { it.isSpecial() }
+     * // Get the first special element since markAtStart, possibly blocking until one is available
+     * val specialElement = record.poll(timeout, markAtStart) { it.isSpecial() }
+     */
+    inner class ReadHead : TrackRecord<E> by this@ArrayTrackRecord {
+        // This lock only controls access to the readHead member below. The ArrayTrackRecord
+        // object has its own synchronization following different (and more usual) semantics.
+        // See the comment on the ReadHead class for details.
+        private val slock = StampedLock()
+        private var readHead = 0
+
+        /**
+         * @return the current value of the mark.
+         */
+        var mark
+            get() = checkThread { readHead }
+            set(v: Int) = rewind(v)
+        fun rewind(v: Int) {
+            val stamp = slock.tryWriteLock()
+            if (0L == stamp) concurrentAccessDetected()
+            readHead = v
+            slock.unlockWrite(stamp)
+        }
+
+        private fun <T> checkThread(r: (Long) -> T): T {
+            // tryOptimisticRead is a read barrier, guarantees writes from other threads are visible
+            // after it
+            val stamp = slock.tryOptimisticRead()
+            val result = r(stamp)
+            // validate also performs a read barrier, guaranteeing that if validate returns true,
+            // then any change either happens-before tryOptimisticRead, or happens-after validate.
+            if (!slock.validate(stamp)) concurrentAccessDetected()
+            return result
+        }
+
+        private fun concurrentAccessDetected(): Nothing {
+            throw ConcurrentModificationException(
+                    "ReadHeads can't be used concurrently. Check your threading model.")
+        }
+
+        /**
+         * Returns the first element after the mark, optionally blocking until one is available, or
+         * null if no such element can be found within the timeout.
+         * If a predicate is given, only elements matching the predicate are returned.
+         *
+         * Upon return the mark will be set to immediately after the returned element, or after
+         * the last element in the queue if null is returned. This means this method will always
+         * skip elements that do not match the predicate, even if it returns null.
+         *
+         * This method can only be used by the thread that created this ManagedRecordingQueue.
+         * If used on another thread, this throws IllegalStateException.
+         *
+         * @param timeoutMs how long, in milliseconds, to wait at most (best effort approximation).
+         * @param predicate an optional predicate to filter elements to be returned.
+         * @return an element matching the predicate, or null if timeout.
+         */
+        fun poll(timeoutMs: Long, predicate: (E) -> Boolean = { true }): E? {
+            val stamp = slock.tryWriteLock()
+            if (0L == stamp) concurrentAccessDetected()
+            try {
+                lock.withLock {
+                    val index = pollForIndexReadLocked(timeoutMs, readHead, predicate)
+                    readHead = if (index < 0) size else index + 1
+                    return getOrNull(index)
+                }
+            } finally {
+                slock.unlockWrite(stamp)
+            }
+        }
+
+        /**
+         * Returns the first element after the mark or null. This never blocks.
+         *
+         * This method is subject to threading restrictions. It can be used concurrently on
+         * multiple threads but not if any other thread might be executing poll() at the same
+         * time. See the class comment for details.
+         */
+        fun peek(): E? = checkThread { getOrNull(readHead) }
+    }
+}
+
+// Private helper
+private fun Condition.await(timeoutMs: Long) = this.await(timeoutMs, TimeUnit.MILLISECONDS)
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
new file mode 100644
index 0000000..af4f96d
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+@file:JvmName("ConcurrentUtils")
+
+package com.android.testutils
+
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.system.measureTimeMillis
+
+// For Java usage
+fun durationOf(fn: Runnable) = measureTimeMillis { fn.run() }
+
+fun CountDownLatch.await(timeoutMs: Long): Boolean = await(timeoutMs, TimeUnit.MILLISECONDS)
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/ExceptionUtils.java b/staticlibs/testutils/hostdevice/com/android/testutils/ExceptionUtils.java
new file mode 100644
index 0000000..d3bda98
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/ExceptionUtils.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 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 java.util.function.Supplier;
+
+/**
+ * A class grouping some utilities to deal with exceptions.
+ */
+public class ExceptionUtils {
+    /**
+     * Like a Consumer, but declared to throw an exception.
+     * @param <T>
+     */
+    @FunctionalInterface
+    public interface ThrowingConsumer<T> {
+        /** @see java.util.function.Consumer */
+        void accept(T t) throws Exception;
+    }
+
+    /**
+     * Like a Supplier, but declared to throw an exception.
+     * @param <T>
+     */
+    @FunctionalInterface
+    public interface ThrowingSupplier<T> {
+        /** @see java.util.function.Supplier */
+        T get() throws Exception;
+    }
+
+    /**
+     * Like a Runnable, but declared to throw an exception.
+     */
+    @FunctionalInterface
+    public interface ThrowingRunnable {
+        /** @see java.lang.Runnable */
+        void run() throws Exception;
+    }
+
+    /**
+     * Convert a supplier that throws into one that doesn't.
+     *
+     * The returned supplier returns null in cases where the source throws.
+     */
+    public static <T> Supplier<T> ignoreExceptions(ThrowingSupplier<T> func) {
+        return () -> {
+            try {
+                return func.get();
+            } catch (Exception e) {
+                return null;
+            }
+        };
+    }
+
+    /**
+     * Convert a runnable that throws into one that doesn't.
+     *
+     * All exceptions are ignored by the returned Runnable.
+     */
+    public static Runnable ignoreExceptions(ThrowingRunnable r) {
+        return () -> {
+            try {
+                r.run();
+            } catch (Exception e) {
+            }
+        };
+    }
+}
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/FileUtils.kt b/staticlibs/testutils/hostdevice/com/android/testutils/FileUtils.kt
new file mode 100644
index 0000000..678f977
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/FileUtils.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 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
+
+// This function is private because the 2 is hardcoded here, and is not correct if not called
+// directly from __LINE__ or __FILE__.
+private fun callerStackTrace(): StackTraceElement = try {
+    throw RuntimeException()
+} catch (e: RuntimeException) {
+    e.stackTrace[2] // 0 is here, 1 is get() in __FILE__ or __LINE__
+}
+val __FILE__: String get() = callerStackTrace().fileName
+val __LINE__: Int get() = callerStackTrace().lineNumber
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
new file mode 100644
index 0000000..a8c6b3c
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+@file:JvmName("MiscAsserts")
+
+package com.android.testutils
+
+import com.android.testutils.ExceptionUtils.ThrowingRunnable
+import java.lang.reflect.Modifier
+import kotlin.system.measureTimeMillis
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+private const val TAG = "Connectivity unit test"
+
+fun <T> assertEmpty(ts: Array<T>) = ts.size.let { len ->
+    assertEquals(0, len, "Expected empty array, but length was $len")
+}
+
+fun <T> assertEmpty(ts: Collection<T>) = ts.size.let { len ->
+    assertEquals(0, len, "Expected empty collection, but length was $len")
+}
+
+fun <T> assertLength(expected: Int, got: Array<T>) = got.size.let { len ->
+    assertEquals(expected, len, "Expected array of length $expected, but was $len for $got")
+}
+
+fun <T> assertLength(expected: Int, got: List<T>) = got.size.let { len ->
+    assertEquals(expected, len, "Expected list of length $expected, but was $len for $got")
+}
+
+// Bridge method to help write this in Java. If you're writing Kotlin, consider using
+// kotlin.test.assertFailsWith instead, as that method is reified and inlined.
+fun <T : Exception> assertThrows(expected: Class<T>, block: ThrowingRunnable): T {
+    return assertFailsWith(expected.kotlin) { block.run() }
+}
+
+fun <T : Exception> assertThrows(msg: String, expected: Class<T>, block: ThrowingRunnable): T {
+    return assertFailsWith(expected.kotlin, msg) { block.run() }
+}
+
+fun <T> assertEqualBothWays(o1: T, o2: T) {
+    assertTrue(o1 == o2)
+    assertTrue(o2 == o1)
+}
+
+fun <T> assertNotEqualEitherWay(o1: T, o2: T) {
+    assertFalse(o1 == o2)
+    assertFalse(o2 == o1)
+}
+
+fun assertStringContains(got: String, want: String) {
+    assertTrue(got.contains(want), "$got did not contain \"${want}\"")
+}
+
+fun assertContainsExactly(actual: IntArray, vararg expected: Int) {
+    // IntArray#sorted() returns a list, so it's fine to test with equals()
+    assertEquals(actual.sorted(), expected.sorted(),
+            "$actual does not contain exactly $expected")
+}
+
+fun assertContainsStringsExactly(actual: Array<String>, vararg expected: String) {
+    assertEquals(actual.sorted(), expected.sorted(),
+            "$actual does not contain exactly $expected")
+}
+
+fun <T> assertContainsAll(list: Collection<T>, vararg elems: T) {
+    assertContainsAll(list, elems.asList())
+}
+
+fun <T> assertContainsAll(list: Collection<T>, elems: Collection<T>) {
+    elems.forEach { assertTrue(list.contains(it), "$it not in list") }
+}
+
+fun assertRunsInAtMost(descr: String, timeLimit: Long, fn: Runnable) {
+    assertRunsInAtMost(descr, timeLimit) { fn.run() }
+}
+
+fun assertRunsInAtMost(descr: String, timeLimit: Long, fn: () -> Unit) {
+    val timeTaken = measureTimeMillis(fn)
+    val msg = String.format("%s: took %dms, limit was %dms", descr, timeTaken, timeLimit)
+    assertTrue(timeTaken <= timeLimit, msg)
+}
+
+/**
+ * Verifies that the number of nonstatic fields in a java class equals a given count.
+ * Note: this is essentially not useful for Kotlin code where fields are not really a thing.
+ *
+ * This assertion serves as a reminder to update test code around it if fields are added
+ * after the test is written.
+ * @param count Expected number of nonstatic fields in the class.
+ * @param clazz Class to test.
+ */
+fun <T> assertFieldCountEquals(count: Int, clazz: Class<T>) {
+    assertEquals(count, clazz.declaredFields.filter {
+        !Modifier.isStatic(it.modifiers) && !Modifier.isTransient(it.modifiers)
+    }.size)
+}
+
+fun assertSameElements(expected: List<String?>, actual: List<String?>) {
+    val expectedSet: HashSet<String?> = HashSet<String?>(expected)
+    assertEquals(expectedSet.size.toLong(), expected.size.toLong(),
+            "expected list contains duplicates")
+    val actualSet: HashSet<String?> = HashSet<String?>(actual)
+    assertEquals(actualSet.size.toLong(), actual.size.toLong(), "actual list contains duplicates")
+    assertEquals(expectedSet, actualSet)
+}
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
new file mode 100644
index 0000000..7c615d0
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 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 java.net.Inet4Address
+import java.util.function.Predicate
+
+const val ETHER_TYPE_OFFSET = 12
+const val ETHER_HEADER_LENGTH = 14
+const val IPV4_PROTOCOL_OFFSET = ETHER_HEADER_LENGTH + 9
+const val IPV4_CHECKSUM_OFFSET = ETHER_HEADER_LENGTH + 10
+const val IPV4_DST_OFFSET = ETHER_HEADER_LENGTH + 16
+const val IPV4_HEADER_LENGTH = 20
+const val IPV4_UDP_OFFSET = ETHER_HEADER_LENGTH + IPV4_HEADER_LENGTH
+const val IPV4_UDP_SRCPORT_OFFSET = IPV4_UDP_OFFSET
+const val IPV4_UDP_DSTPORT_OFFSET = IPV4_UDP_OFFSET + 2
+const val UDP_HEADER_LENGTH = 8
+const val BOOTP_OFFSET = IPV4_UDP_OFFSET + UDP_HEADER_LENGTH
+const val BOOTP_TID_OFFSET = BOOTP_OFFSET + 4
+const val BOOTP_CLIENT_MAC_OFFSET = BOOTP_OFFSET + 28
+const val DHCP_OPTIONS_OFFSET = BOOTP_OFFSET + 240
+
+const val ARP_OPCODE_OFFSET = ETHER_HEADER_LENGTH + 6
+const val ARP_SENDER_MAC_OFFSET = ETHER_HEADER_LENGTH + 8
+const val ARP_TARGET_IPADDR_OFFSET = ETHER_HEADER_LENGTH + 24
+
+/**
+ * A [Predicate] that matches a [ByteArray] if it contains the specified [bytes] at the specified
+ * [offset].
+ */
+class OffsetFilter(val offset: Int, vararg val bytes: Byte) : Predicate<ByteArray> {
+    override fun test(packet: ByteArray) =
+            bytes.withIndex().all { it.value == packet[offset + it.index] }
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped packets that contain an UDP over IPv4 datagram.
+ */
+class IPv4UdpFilter : Predicate<ByteArray> {
+    private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x08, 0x00 /* IPv4 */).and(
+            OffsetFilter(IPV4_PROTOCOL_OFFSET, 17 /* UDP */))
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped packets sent to the specified IPv4 destination.
+ */
+class IPv4DstFilter(dst: Inet4Address) : Predicate<ByteArray> {
+    private val impl = OffsetFilter(IPV4_DST_OFFSET, *dst.address)
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped ARP requests.
+ */
+class ArpRequestFilter : Predicate<ByteArray> {
+    private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x08, 0x06 /* ARP */)
+            .and(OffsetFilter(ARP_OPCODE_OFFSET, 0x00, 0x01 /* request */))
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped DHCP packets sent from a DHCP client.
+ */
+class DhcpClientPacketFilter : Predicate<ByteArray> {
+    private val impl = IPv4UdpFilter()
+            .and(OffsetFilter(IPV4_UDP_SRCPORT_OFFSET, 0x00, 0x44 /* 68 */))
+            .and(OffsetFilter(IPV4_UDP_DSTPORT_OFFSET, 0x00, 0x43 /* 67 */))
+    override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches a [ByteArray] if it contains a ethernet-encapped DHCP packet that
+ * contains the specified option with the specified [bytes] as value.
+ */
+class DhcpOptionFilter(val option: Byte, vararg val bytes: Byte) : Predicate<ByteArray> {
+    override fun test(packet: ByteArray): Boolean {
+        val option = findDhcpOption(packet, option) ?: return false
+        return option.contentEquals(bytes)
+    }
+}
+
+/**
+ * Find a DHCP option in a packet and return its value, if found.
+ */
+fun findDhcpOption(packet: ByteArray, option: Byte): ByteArray? =
+        findOptionOffset(packet, option, DHCP_OPTIONS_OFFSET)?.let {
+            val optionLen = packet[it + 1]
+            return packet.copyOfRange(it + 2 /* type, length bytes */, it + 2 + optionLen)
+        }
+
+private tailrec fun findOptionOffset(packet: ByteArray, option: Byte, searchOffset: Int): Int? {
+    if (packet.size <= searchOffset + 2 /* type, length bytes */) return null
+
+    return if (packet[searchOffset] == option) searchOffset else {
+        val optionLen = packet[searchOffset + 1]
+        findOptionOffset(packet, option, searchOffset + 2 + optionLen)
+    }
+}
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/SkipPresubmit.kt b/staticlibs/testutils/hostdevice/com/android/testutils/SkipPresubmit.kt
new file mode 100644
index 0000000..69ed048
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/SkipPresubmit.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2020 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
+
+/**
+ * Skip the test in presubmit runs for the reason specified in [reason].
+ *
+ * This annotation is typically used to document hardware or test bench limitations.
+ */
+annotation class SkipPresubmit(val reason: String)
\ No newline at end of file