Merge remote-tracking branch 'remotes/aosp/tmp_libs_net_move' into libs_net_move_merge
frameworks/libs/net/common ->
packages/modules/Connectivity/staticlibs
frameworks/libs/net/client-libs ->
packages/modules/Connectivity/staticlbs/client-libs
Test: TH
Bug: 296014682
Change-Id: I5dc78f0c4653e20312ab3d488b1e69262dbb9840
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
new file mode 100644
index 0000000..5fe7ac3
--- /dev/null
+++ b/staticlibs/testutils/Android.bp
@@ -0,0 +1,88 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "net-tests-utils",
+ srcs: [
+ "devicetests/**/*.java",
+ "devicetests/**/*.kt",
+ ],
+ defaults: [
+ "framework-connectivity-test-defaults",
+ "lib_mockito_extended"
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "net-utils-device-common-bpf", // TestBpfMap extends IBpfMap.
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "kotlin-reflect",
+ "libnanohttpd",
+ "net-tests-utils-host-device-common",
+ "net-utils-device-common",
+ "net-utils-device-common-async",
+ "net-utils-device-common-netlink",
+ "net-utils-device-common-wear",
+ "modules-utils-build_system",
+ ],
+ lint: { strict_updatability_linting: true },
+}
+
+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: [
+ "//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
+ "//packages/modules/Connectivity/staticlibs/client-libs/tests:__subpackages__",
+ "//packages/modules/Connectivity/tests/cts/hostside",
+ ],
+ // There are downstream branches using an old version of Kotlin
+ // that used to reserve the right to make breaking changes to the
+ // Result type and disallowed returning an instance of it.
+ // Later versions allowed this and there was never a change,
+ // so no matter the version returning Result is always fine,
+ // but on sc-mainline-prod the compiler rejects it without
+ // the following flag.
+ kotlincflags: ["-Xallow-result-return-type"],
+ libs: [
+ "jsr305",
+ ],
+ static_libs: [
+ "kotlin-test"
+ ],
+ lint: { strict_updatability_linting: true },
+}
+
+java_test_host {
+ name: "net-tests-utils-host-common",
+ srcs: [
+ "host/**/*.java",
+ "host/**/*.kt",
+ ],
+ libs: ["tradefed"],
+ test_suites: ["ats", "device-tests", "general-tests", "cts", "mts-networking"],
+ data: [":ConnectivityTestPreparer"],
+}
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
new file mode 100644
index 0000000..f7118cf
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -0,0 +1,34 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "ConnectivityTestPreparer",
+ srcs: ["src/**/*.kt"],
+ sdk_version: "system_current",
+ // Allow running the test on any device with SDK Q+, even when built from a branch that uses
+ // an unstable SDK, by targeting a stable SDK regardless of the build SDK.
+ min_sdk_version: "29",
+ target_sdk_version: "30",
+ static_libs: [
+ "androidx.test.rules",
+ "modules-utils-build_system",
+ "net-tests-utils",
+ ],
+ host_required: ["net-tests-utils-host-common"],
+ lint: { strict_updatability_linting: true },
+}
diff --git a/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml b/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml
new file mode 100644
index 0000000..015b41f
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.testutils.connectivitypreparer">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <!-- For wifi scans -->
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.testutils.connectivitypreparer"
+ android:label="Connectivity test target preparer" />
+</manifest>
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
new file mode 100644
index 0000000..f1f0975
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.connectivitypreparer
+
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkRequest
+import android.telephony.TelephonyManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectUtil
+import com.android.testutils.RecorderCallback
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.tryTest
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ConnectivityCheckTest {
+ val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ val pm by lazy { context.packageManager }
+
+ @Test
+ fun testCheckConnectivity() {
+ checkWifiSetup()
+ checkTelephonySetup()
+ }
+
+ private fun checkWifiSetup() {
+ if (!pm.hasSystemFeature(FEATURE_WIFI)) return
+ ConnectUtil(context).ensureWifiConnected()
+ }
+
+ private fun checkTelephonySetup() {
+ if (!pm.hasSystemFeature(FEATURE_TELEPHONY)) return
+ val tm = context.getSystemService(TelephonyManager::class.java)
+ ?: fail("Could not get telephony service")
+
+ val commonError = "Check the test bench. To run the tests anyway for quick & dirty local " +
+ "testing, you can use atest X -- " +
+ "--test-arg com.android.testutils.ConnectivityTestTargetPreparer" +
+ ":ignore-connectivity-check:true"
+ // Do not use assertEquals: it outputs "expected X, was Y", which looks like a test failure
+ if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) {
+ fail("The device has no SIM card inserted. $commonError")
+ } else if (tm.simState != TelephonyManager.SIM_STATE_READY) {
+ fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
+ commonError)
+ }
+ assertTrue(tm.isDataConnectivityPossible,
+ "The device is not setup with a SIM card that supports data connectivity. " +
+ commonError)
+ val cb = TestableNetworkCallback()
+ val cm = context.getSystemService(ConnectivityManager::class.java)
+ ?: fail("Could not get ConnectivityManager")
+ cm.requestNetwork(
+ NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+ tryTest {
+ cb.poll { it is RecorderCallback.CallbackEntry.Available }
+ ?: fail("The device does not have mobile data available. Check that it is " +
+ "setup with a SIM card that has a working data plan, and that the " +
+ "APN configuration is valid.")
+ } cleanup {
+ cm.unregisterNetworkCallback(cb)
+ }
+ }
+}
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..cf0490c
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ArpResponder.kt
@@ -0,0 +1,64 @@
+/*
+ * 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 com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import java.net.Inet4Address
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+private const val ARP_SENDER_MAC_OFFSET = ETHER_HEADER_LEN + 8
+private const val ARP_TARGET_IPADDR_OFFSET = ETHER_HEADER_LEN + 24
+
+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..82f1d9b
--- /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 com.android.modules.utils.build.SdkLevel.isAtLeastS
+
+/**
+ * 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 (!isAtLeastS()) {
+ 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 (!isAtLeastS()) {
+ 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
+ }
+}
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..9e72f4b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConcurrentInterpreter.kt
@@ -0,0 +1,240 @@
+package com.android.testutils
+
+import android.os.SystemClock
+import java.util.concurrent.CyclicBarrier
+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
+const val INTERPRET_TIME_UNIT = 60L // ms
+
+/**
+ * A small interpreter for testing parallel code.
+ *
+ * The interpreter will read a list of lines consisting of "|"-separated statements, e.g. :
+ * sleep 2 ; unblock thread2 | wait thread2 time 2..5
+ * sendMessage "x" | obtainMessage = "x" time 0..1
+ *
+ * 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. Refer to
+ * getDefaultInstructions() below for a list and documentation.
+ */
+open class ConcurrentInterpreter<T>(localInterpretTable: List<InterpretMatcher<T>>) {
+ private val interpretTable: List<InterpretMatcher<T>> =
+ localInterpretTable + getDefaultInstructions()
+ // The last time the thread became blocked, with base System.currentTimeMillis(). This should
+ // be set immediately before any time the thread gets blocked.
+ internal val lastBlockedTime = ThreadLocal<Long>()
+
+ // 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.
+ *
+ * All threads wait 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.
+ *
+ * This method is mostly meant for implementations that extend the ConcurrentInterpreter
+ * class to add their own directives and instructions. These may need to operate on some
+ * data, which can be passed in |initial|. For example, an interpreter specialized in callbacks
+ * may want to pass the callback there. In some cases, it's necessary that each thread
+ * performs a transformation *after* it starts on that value before starting ; in this case,
+ * the transformation can be passed to |threadTransform|. The default is to return |initial| as
+ * is. Look at some existing child classes of this interpreter for some examples of how this
+ * can be used.
+ *
+ * @param spec The test spec, as a string of lines separated by pipes.
+ * @param initial An initial value passed to all threads.
+ * @param lineShift How many lines after the call the spec starts, for error reporting.
+ * @param threadTransform an optional transformation that each thread will apply to |initial|
+ */
+ 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("|") }
+ // |lines| 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.
+ // E.g. if the string is """
+ // a | b | c
+ // d | e | f
+ // """, then lines is [ [ "a", "b", "c" ], [ "d", "e", "f" ] ].
+ val threadCount = lines[0].size
+ assertTrue(lines.all { it.size == threadCount })
+ val threadInstructions = (0 until threadCount).map { i -> lines.map { it[i].trim() } }
+ // |threadInstructions| is a list where each element is the list of instructions for the
+ // thread at the index. In other words, it's just |lines| transposed. In the example
+ // above, it would be [ [ "a", "d" ], [ "b", "e" ], [ "c", "f" ] ]
+ // mapIndexed below will pass in |instructions| the list of instructions for this thread.
+ val barrier = CyclicBarrier(threadCount)
+ var crash: InterpretException? = null
+ threadInstructions.mapIndexed { threadIndex, instructions ->
+ Thread {
+ val threadLocal = threadTransform(initial)
+ lastBlockedTime.set(System.currentTimeMillis())
+ 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)
+ }
+ lastBlockedTime.set(System.currentTimeMillis())
+ 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
+ }
+}
+
+/**
+ * Default instructions available to all interpreters.
+ * 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.
+ * EXPR fails : checks that EXPR throws some exception.
+ */
+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 lateStart = System.currentTimeMillis()
+ i.interpret(r.strArg(1), t)
+ val end = System.currentTimeMillis()
+ // There is uncertainty in measuring time.
+ // It takes some (small) time for the thread to even measure the time at which it
+ // starts interpreting the instruction. It is therefore possible that thread A sleeps for
+ // n milliseconds, and B expects to have waited for at least n milliseconds, but because
+ // B started measuring after 1ms or so, B thinks it didn't wait long enough.
+ // To avoid this, when the `time` instruction tests the instruction took at least X and
+ // at most Y, it tests X against a time measured since *before* the thread blocked but
+ // Y against a time measured as late as possible. This ensures that the timer is
+ // sufficiently lenient in both directions that there are no flaky measures.
+ val minTime = end - lateStart
+ val maxTime = end - i.lastBlockedTime.get()!!
+
+ assertTrue(maxTime >= r.timeArg(2),
+ "Should have taken at least ${r.timeArg(2)} but took less than $maxTime")
+ assertTrue(minTime <= r.timeArg(3),
+ "Should have taken at most ${r.timeArg(3)} but took more than $minTime")
+ },
+ // 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()) INTERPRET_TIME_UNIT 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/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
new file mode 100644
index 0000000..71f7877
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.Manifest.permission
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.ScanResult
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.testutils.RecorderCallback.CallbackEntry
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MAX_WIFI_CONNECT_RETRIES = 10
+private const val WIFI_CONNECT_INTERVAL_MS = 500L
+private const val WIFI_CONNECT_TIMEOUT_MS = 30_000L
+
+// Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi,
+// the error code constants are not (b/204277752)
+private const val WIFI_ERROR_IN_PROGRESS = 1
+private const val WIFI_ERROR_BUSY = 2
+
+class ConnectUtil(private val context: Context) {
+ private val TAG = ConnectUtil::class.java.simpleName
+
+ private val cm = context.getSystemService(ConnectivityManager::class.java)
+ ?: fail("Could not find ConnectivityManager")
+ private val wifiManager = context.getSystemService(WifiManager::class.java)
+ ?: fail("Could not find WifiManager")
+
+ fun ensureWifiConnected(): Network {
+ val callback = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build(), callback)
+
+ try {
+ val connInfo = wifiManager.connectionInfo
+ Log.d(TAG, "connInfo=" + connInfo)
+ if (connInfo == null || connInfo.networkId == -1) {
+ clearWifiBlocklist()
+ val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable")
+ // Read the output stream to ensure the command has completed
+ ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
+ val config = getOrCreateWifiConfiguration()
+ connectToWifiConfig(config)
+ }
+ val cb = callback.poll(WIFI_CONNECT_TIMEOUT_MS) { it is CallbackEntry.Available }
+ assertNotNull(cb, "Could not connect to a wifi access point within " +
+ "$WIFI_CONNECT_TIMEOUT_MS ms. Check that the test device has a wifi network " +
+ "configured, and that the test access point is functioning properly.")
+ return cb.network
+ } finally {
+ cm.unregisterNetworkCallback(callback)
+ }
+ }
+
+ private fun connectToWifiConfig(config: WifiConfiguration) {
+ repeat(MAX_WIFI_CONNECT_RETRIES) {
+ val error = runAsShell(permission.NETWORK_SETTINGS) {
+ val listener = ConnectWifiListener()
+ wifiManager.connect(config, listener)
+ listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ } ?: return // Connect succeeded
+
+ // Only retry for IN_PROGRESS and BUSY
+ if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
+ fail("Failed to connect to " + config.SSID + ": " + error)
+ }
+ Log.w(TAG, "connect failed with $error; waiting before retry")
+ SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS)
+ }
+ fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries")
+ }
+
+ private class ConnectWifiListener : WifiManager.ActionListener {
+ /**
+ * Future completed when the connect process ends. Provides the error code or null if none.
+ */
+ val connectFuture = CompletableFuture<Int?>()
+ override fun onSuccess() {
+ connectFuture.complete(null)
+ }
+
+ override fun onFailure(reason: Int) {
+ connectFuture.complete(reason)
+ }
+ }
+
+ private fun getOrCreateWifiConfiguration(): WifiConfiguration {
+ val configs = runAsShell(permission.NETWORK_SETTINGS) {
+ wifiManager.getConfiguredNetworks()
+ }
+ // If no network is configured, add a config for virtual access points if applicable
+ if (configs.size == 0) {
+ val scanResults = getWifiScanResults()
+ val virtualConfig = maybeConfigureVirtualNetwork(scanResults)
+ assertNotNull(virtualConfig, "The device has no configured wifi network")
+ return virtualConfig
+ }
+ // No need to add a configuration: there is already one.
+ if (configs.size > 1) {
+ // For convenience in case of local testing on devices with multiple saved configs,
+ // prefer the first configuration that is in range.
+ // In actual tests, there should only be one configuration, and it should be usable as
+ // assumed by WifiManagerTest.testConnect.
+ Log.w(TAG, "Multiple wifi configurations found: " +
+ configs.joinToString(", ") { it.SSID })
+ val scanResultsList = getWifiScanResults()
+ Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") {
+ "${it.SSID} (${it.level})"
+ })
+
+ val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet()
+ return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0]
+ }
+ return configs[0]
+ }
+
+ private fun getWifiScanResults(): List<ScanResult> {
+ val scanResultsFuture = CompletableFuture<List<ScanResult>>()
+ runAsShell(permission.NETWORK_SETTINGS) {
+ val receiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ scanResultsFuture.complete(wifiManager.scanResults)
+ }
+ }
+ context.registerReceiver(receiver,
+ IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
+ wifiManager.startScan()
+ }
+ return try {
+ scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ } catch (e: Exception) {
+ throw AssertionError("Wifi scan results not received within timeout", e)
+ }
+ }
+
+ /**
+ * If a virtual wifi network is detected, add a configuration for that network.
+ * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
+ */
+ private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
+ // Virtual wifi networks used on the emulator and cloud testing infrastructure
+ val virtualSsids = listOf("VirtWifi", "AndroidWifi")
+ Log.d(TAG, "Wifi scan results: $scanResults")
+ val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
+ ?: return null
+
+ // Only add the virtual configuration if the virtual AP is detected in scans
+ val virtualConfig = WifiConfiguration()
+ // ASCII SSIDs need to be surrounded by double quotes
+ virtualConfig.SSID = "\"${virtualScanResult.SSID}\""
+ virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
+ runAsShell(permission.NETWORK_SETTINGS) {
+ val networkId = wifiManager.addNetwork(virtualConfig)
+ assertTrue(networkId >= 0)
+ assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */))
+ }
+ return virtualConfig
+ }
+
+ /**
+ * Re-enable wifi networks that were blocked, typically because no internet connection was
+ * detected the last time they were connected. This is necessary to make sure wifi can reconnect
+ * to them.
+ */
+ private fun clearWifiBlocklist() {
+ runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) {
+ for (cfg in wifiManager.configuredNetworks) {
+ assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */))
+ }
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ContextUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/ContextUtils.kt
new file mode 100644
index 0000000..936b568
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ContextUtils.kt
@@ -0,0 +1,67 @@
+/*
+ * 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("ContextUtils")
+
+package com.android.testutils
+
+import android.content.Context
+import android.os.UserHandle
+import org.mockito.AdditionalAnswers.delegatesTo
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import java.util.function.BiConsumer
+
+// Helper function so that Java doesn't have to pass a method that returns Unit
+fun mockContextAsUser(context: Context, functor: BiConsumer<Context, UserHandle>? = null) =
+ mockContextAsUser(context) { c, h -> functor?.accept(c, h) }
+
+/**
+ * Return a context with assigned user and delegate to original context.
+ *
+ * @param context the mock context to set up createContextAsUser on. After this function
+ * is called, client code can call createContextAsUser and expect a context that
+ * will return the correct user and userId.
+ *
+ * @param functor additional code to run on the created context-as-user instances, for example to
+ * set up further mocks on these contexts.
+ */
+fun mockContextAsUser(context: Context, functor: ((Context, UserHandle) -> Unit)? = null) {
+ doAnswer { invocation ->
+ val asUserContext = mock(Context::class.java, delegatesTo<Context>(context))
+ val user = invocation.arguments[0] as UserHandle
+ val userId = user.identifier
+ doReturn(user).`when`(asUserContext).user
+ doReturn(userId).`when`(asUserContext).userId
+ functor?.let { it(asUserContext, user) }
+ asUserContext
+ }.`when`(context).createContextAsUser(any(UserHandle::class.java), anyInt() /* flags */)
+}
+
+/**
+ * Helper function to mock the desired system service.
+ *
+ * @param context the mock context to set up the getSystemService and getSystemServiceName.
+ * @param clazz the system service class that intents to mock.
+ * @param service the system service name that intents to mock.
+ */
+fun <T> mockService(context: Context, clazz: Class<T>, name: String, service: T) {
+ doReturn(service).`when`(context).getSystemService(name)
+ doReturn(name).`when`(context).getSystemServiceName(clazz)
+}
\ No newline at end of file
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..35f22b9
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -0,0 +1,144 @@
+/*
+ * 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 androidx.test.InstrumentationRegistry
+import com.android.modules.utils.build.UnboundedSdkLevel
+import java.util.regex.Pattern
+import org.junit.Assume.assumeTrue
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+@Deprecated("Use Build.VERSION_CODES", ReplaceWith("Build.VERSION_CODES.S_V2"))
+const val SC_V2 = Build.VERSION_CODES.S_V2
+
+private val MAX_TARGET_SDK_ANNOTATION_RE = Pattern.compile("MaxTargetSdk([0-9]+)$")
+private val targetSdk = InstrumentationRegistry.getContext().applicationInfo.targetSdkVersion
+
+private fun isDevSdkInRange(minExclusive: String?, maxInclusive: String?): Boolean {
+ return (minExclusive == null || !isAtMost(minExclusive)) &&
+ (maxInclusive == null || isAtMost(maxInclusive))
+}
+
+private fun isAtMost(sdkVersionOrCodename: String): Boolean {
+ // UnboundedSdkLevel does not support builds < Q, and may stop supporting Q as well since it
+ // is intended for mainline modules that are now R+.
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+ // Assume that any codename passed as argument from current code is a more recent build than
+ // Q: this util did not exist before Q, and codenames are only used before the corresponding
+ // build is finalized. This util could list 28 older codenames to check against (as per
+ // ro.build.version.known_codenames in more recent builds), but this does not seem valuable.
+ val intVersion = sdkVersionOrCodename.toIntOrNull() ?: return true
+ return Build.VERSION.SDK_INT <= intVersion
+ }
+ return UnboundedSdkLevel.isAtMost(sdkVersionOrCodename)
+}
+
+/**
+ * Returns true if the development SDK version of the device is in the provided annotation range.
+ *
+ * If the device is not using a release SDK, the development SDK differs from
+ * [Build.VERSION.SDK_INT], and is indicated by the device codenames; see [UnboundedSdkLevel].
+ */
+fun isDevSdkInRange(
+ ignoreUpTo: DevSdkIgnoreRule.IgnoreUpTo?,
+ ignoreAfter: DevSdkIgnoreRule.IgnoreAfter?
+): Boolean {
+ val minExclusive =
+ if (ignoreUpTo?.value == 0) ignoreUpTo.codename
+ else ignoreUpTo?.value?.toString()
+ val maxInclusive =
+ if (ignoreAfter?.value == 0) ignoreAfter.codename
+ else ignoreAfter?.value?.toString()
+ return isDevSdkInRange(minExclusive, maxInclusive)
+}
+
+private fun getMaxTargetSdk(description: Description): Int? {
+ return description.annotations.firstNotNullOfOrNull {
+ MAX_TARGET_SDK_ANNOTATION_RE.matcher(it.annotationClass.simpleName).let { m ->
+ if (m.find()) m.group(1).toIntOrNull() else null
+ }
+ }
+}
+
+/**
+ * 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 codename or
+ * SDK level.
+ * @param ignoreClassAfter Skip all tests in the class if the device dev SDK is > this codename or
+ * SDK level.
+ */
+class DevSdkIgnoreRule @JvmOverloads constructor(
+ private val ignoreClassUpTo: String? = null,
+ private val ignoreClassAfter: String? = null
+) : TestRule {
+ /**
+ * @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.
+ */
+ @JvmOverloads
+ constructor(ignoreClassUpTo: Int?, ignoreClassAfter: Int? = null) : this(
+ ignoreClassUpTo?.toString(), ignoreClassAfter?.toString())
+
+ 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 = 0, val codename: String = "")
+
+ /**
+ * 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 = 0, val codename: String = "")
+
+ 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 devSdkMessage = "Skipping test for build ${Build.VERSION.CODENAME} " +
+ "with SDK ${Build.VERSION.SDK_INT}"
+ assumeTrue(devSdkMessage, isDevSdkInRange(ignoreClassUpTo, ignoreClassAfter))
+ assumeTrue(devSdkMessage, isDevSdkInRange(ignoreUpTo, ignoreAfter))
+
+ val maxTargetSdk = getMaxTargetSdk(description)
+ if (maxTargetSdk != null) {
+ assumeTrue("Skipping test, target SDK $targetSdk greater than $maxTargetSdk",
+ targetSdk <= maxTargetSdk)
+ }
+ base.evaluate()
+ }
+ }
+}
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..2e73666
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.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 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.manipulation.Filter
+import org.junit.runner.manipulation.Filterable
+import org.junit.runner.manipulation.NoTestsRemainException
+import org.junit.runner.manipulation.Sortable
+import org.junit.runner.manipulation.Sorter
+import org.junit.runner.notification.RunNotifier
+import kotlin.jvm.Throws
+
+/**
+ * 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(), Filterable, Sortable {
+ private val baseRunner = klass.let {
+ val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
+ val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
+
+ if (isDevSdkInRange(ignoreUpTo, ignoreAfter)) 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, as the class is expected to
+ // report results when run. In practice runners that apply the Filterable implementation
+ // would see a NoTestsRemainException and not call the run method.
+ notifier.fireTestIgnored(
+ Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+ }
+
+ override fun getDescription(): Description {
+ return baseRunner?.description ?: Description.createSuiteDescription(klass)
+ }
+
+ /**
+ * Get the test count before applying the [Filterable] implementation.
+ */
+ override fun testCount(): Int {
+ // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
+ return baseRunner?.testCount() ?: 1
+ }
+
+ @Throws(NoTestsRemainException::class)
+ override fun filter(filter: Filter?) {
+ baseRunner?.filter(filter) ?: throw NoTestsRemainException()
+ }
+
+ override fun sort(sorter: Sorter?) {
+ baseRunner?.sort(sorter)
+ }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
new file mode 100644
index 0000000..3d98cc3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 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.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+
+private val TAG = DeviceConfigRule::class.simpleName
+
+private const val TIMEOUT_MS = 20_000L
+
+/**
+ * A [TestRule] that helps set [DeviceConfig] for tests and clean up the test configuration
+ * automatically on teardown.
+ *
+ * The rule can also optionally retry tests when they fail following an external change of
+ * DeviceConfig before S; this typically happens because device config flags are synced while the
+ * test is running, and DisableConfigSyncTargetPreparer is only usable starting from S.
+ *
+ * @param retryCountBeforeSIfConfigChanged if > 0, when the test fails before S, check if
+ * the configs that were set through this rule were changed, and retry the test
+ * up to the specified number of times if yes.
+ */
+class DeviceConfigRule @JvmOverloads constructor(
+ val retryCountBeforeSIfConfigChanged: Int = 0
+) : TestRule {
+ // Maps (namespace, key) -> value
+ private val originalConfig = mutableMapOf<Pair<String, String>, String?>()
+ private val usedConfig = mutableMapOf<Pair<String, String>, String?>()
+
+ /**
+ * Actions to be run after cleanup of the config, for the current test only.
+ */
+ private val currentTestCleanupActions = mutableListOf<ThrowingRunnable>()
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return TestValidationUrlStatement(base, description)
+ }
+
+ private inner class TestValidationUrlStatement(
+ private val base: Statement,
+ private val description: Description
+ ) : Statement() {
+ override fun evaluate() {
+ var retryCount = if (SdkLevel.isAtLeastS()) 1 else retryCountBeforeSIfConfigChanged + 1
+ while (retryCount > 0) {
+ retryCount--
+ tryTest {
+ base.evaluate()
+ // Can't use break/return out of a loop here because this is a tryTest lambda,
+ // so set retryCount to exit instead
+ retryCount = 0
+ }.catch<Throwable> { e -> // junit AssertionFailedError does not extend Exception
+ if (retryCount == 0) throw e
+ usedConfig.forEach { (key, value) ->
+ val currentValue = runAsShell(READ_DEVICE_CONFIG) {
+ DeviceConfig.getProperty(key.first, key.second)
+ }
+ if (currentValue != value) {
+ Log.w(TAG, "Test failed with unexpected device config change, retrying")
+ return@catch
+ }
+ }
+ throw e
+ } cleanupStep {
+ runAsShell(WRITE_DEVICE_CONFIG) {
+ originalConfig.forEach { (key, value) ->
+ DeviceConfig.setProperty(
+ key.first, key.second, value, false /* makeDefault */)
+ }
+ }
+ } cleanupStep {
+ originalConfig.clear()
+ usedConfig.clear()
+ } cleanup {
+ // Fold all cleanup actions into cleanup steps of an empty tryTest, so they are
+ // all run even if exceptions are thrown, and exceptions are reported properly.
+ currentTestCleanupActions.fold(tryTest { }) {
+ tryBlock, action -> tryBlock.cleanupStep { action.run() }
+ }.cleanup {
+ currentTestCleanupActions.clear()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Set a configuration key/value. After the test case ends, it will be restored to the value it
+ * had when this method was first called.
+ */
+ fun setConfig(namespace: String, key: String, value: String?): String? {
+ Log.i(TAG, "Setting config \"$key\" to \"$value\"")
+ val readWritePermissions = arrayOf(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+
+ val keyPair = Pair(namespace, key)
+ val existingValue = runAsShell(*readWritePermissions) {
+ DeviceConfig.getProperty(namespace, key)
+ }
+ if (!originalConfig.containsKey(keyPair)) {
+ originalConfig[keyPair] = existingValue
+ }
+ usedConfig[keyPair] = value
+ if (existingValue == value) {
+ // Already the correct value. There may be a race if a change is already in flight,
+ // but if multiple threads update the config there is no way to fix that anyway.
+ Log.i(TAG, "\"$key\" already had value \"$value\"")
+ return value
+ }
+
+ val future = CompletableFuture<String>()
+ val listener = DeviceConfig.OnPropertiesChangedListener {
+ // The listener receives updates for any change to any key, so don't react to
+ // changes that do not affect the relevant key
+ if (!it.keyset.contains(key)) return@OnPropertiesChangedListener
+ // "null" means absent in DeviceConfig : there is no such thing as a present but
+ // null value, so the following works even if |value| is null.
+ if (it.getString(key, null) == value) {
+ future.complete(value)
+ }
+ }
+
+ return tryTest {
+ runAsShell(*readWritePermissions) {
+ DeviceConfig.addOnPropertiesChangedListener(
+ DeviceConfig.NAMESPACE_CONNECTIVITY,
+ inlineExecutor,
+ listener)
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_CONNECTIVITY,
+ key,
+ value,
+ false /* makeDefault */)
+ // Don't drop the permission until the config is applied, just in case
+ future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }.also {
+ Log.i(TAG, "Config \"$key\" successfully set to \"$value\"")
+ }
+ } cleanup {
+ DeviceConfig.removeOnPropertiesChangedListener(listener)
+ }
+ }
+
+ private val inlineExecutor get() = Executor { r -> r.run() }
+
+ /**
+ * Add an action to be run after config cleanup when the current test case ends.
+ */
+ fun runAfterNextCleanup(action: ThrowingRunnable) {
+ currentTestCleanupActions.add(action)
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
new file mode 100644
index 0000000..ce55fdc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2022 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.VintfRuntimeInfo;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for device information.
+ */
+public class DeviceInfoUtils {
+ /**
+ * Class for a three-part kernel version number.
+ */
+ public static class KVersion {
+ public final int major;
+ public final int minor;
+ public final int sub;
+
+ public KVersion(int major, int minor, int sub) {
+ this.major = major;
+ this.minor = minor;
+ this.sub = sub;
+ }
+
+ /**
+ * Compares with other version numerically.
+ *
+ * @param other the other version to compare
+ * @return the value 0 if this == other;
+ * a value less than 0 if this < other and
+ * a value greater than 0 if this > other.
+ */
+ public int compareTo(final KVersion other) {
+ int res = Integer.compare(this.major, other.major);
+ if (res == 0) {
+ res = Integer.compare(this.minor, other.minor);
+ }
+ if (res == 0) {
+ res = Integer.compare(this.sub, other.sub);
+ }
+ return res;
+ }
+
+ /**
+ * At least satisfied with the given version.
+ *
+ * @param from the start version to compare
+ * @return return true if this version is at least satisfied with the given version.
+ * otherwise, return false.
+ */
+ public boolean isAtLeast(final KVersion from) {
+ return compareTo(from) >= 0;
+ }
+
+ /**
+ * Falls within the given range [from, to).
+ *
+ * @param from the start version to compare
+ * @param to the end version to compare
+ * @return return true if this version falls within the given range.
+ * otherwise, return false.
+ */
+ public boolean isInRange(final KVersion from, final KVersion to) {
+ return isAtLeast(from) && !isAtLeast(to);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof KVersion)) return false;
+ KVersion that = (KVersion) o;
+ return this.major == that.major
+ && this.minor == that.minor
+ && this.sub == that.sub;
+ }
+ };
+
+ /**
+ * Get a two-part kernel version number (major and minor) from a given string.
+ *
+ * TODO: use class KVersion.
+ */
+ private static Pair<Integer, Integer> getMajorMinorVersion(String version) {
+ // Only gets major and minor number of the version string.
+ final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
+ final Matcher m = versionPattern.matcher(version);
+ if (m.matches()) {
+ final int major = Integer.parseInt(m.group(1));
+ final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3));
+ return new Pair<>(major, minor);
+ } else {
+ return new Pair<>(0, 0);
+ }
+ }
+
+ /**
+ * Compares two version strings numerically. Compare only major and minor number of the
+ * version string. The version comparison uses #Integer.compare. Possible version
+ * 5, 5.10, 5-beta1, 4.8-RC1, 4.7.10.10 and so on.
+ *
+ * @param s1 the first version string to compare
+ * @param s2 the second version string to compare
+ * @return the value 0 if s1 == s2;
+ * a value less than 0 if s1 < s2 and
+ * a value greater than 0 if s1 > s2.
+ *
+ * TODO: use class KVersion.
+ */
+ public static int compareMajorMinorVersion(final String s1, final String s2) {
+ final Pair<Integer, Integer> v1 = getMajorMinorVersion(s1);
+ final Pair<Integer, Integer> v2 = getMajorMinorVersion(s2);
+
+ if (Objects.equals(v1.first, v2.first)) {
+ return Integer.compare(v1.second, v2.second);
+ } else {
+ return Integer.compare(v1.first, v2.first);
+ }
+ }
+
+ /**
+ * Get a three-part kernel version number (major, minor and subminor) from a given string.
+ * Any version string must at least have major and minor number. If the subminor number can't
+ * be parsed from string. Assign zero as subminor number. Invalid version is treated as
+ * version 0.0.0.
+ */
+ public static KVersion getMajorMinorSubminorVersion(final String version) {
+ // The kernel version is a three-part version number (major, minor and subminor). Get
+ // the three-part version numbers and discard the remaining stuff if any.
+ // For example:
+ // 4.19.220-g500ede0aed22-ab8272303 --> 4.19.220
+ // 5.17-rc6-g52099515ca00-ab8032400 --> 5.17.0
+ final Pattern versionPattern = Pattern.compile("^(\\d+)\\.(\\d+)(\\.(\\d+))?.*");
+ final Matcher m = versionPattern.matcher(version);
+ if (m.matches()) {
+ final int major = Integer.parseInt(m.group(1));
+ final int minor = Integer.parseInt(m.group(2));
+ final int sub = TextUtils.isEmpty(m.group(4)) ? 0 : Integer.parseInt(m.group(4));
+ return new KVersion(major, minor, sub);
+ } else {
+ return new KVersion(0, 0, 0);
+ }
+ }
+
+ /**
+ * Check if the current kernel version is at least satisfied with the given version.
+ *
+ * @param version the start version to compare
+ * @return return true if the current version is at least satisfied with the given version.
+ * otherwise, return false.
+ */
+ public static boolean isKernelVersionAtLeast(final String version) {
+ final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
+ final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
+ final KVersion from = DeviceInfoUtils.getMajorMinorSubminorVersion(version);
+ return current.isAtLeast(from);
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DnsAnswerProvider.kt b/staticlibs/testutils/devicetests/com/android/testutils/DnsAnswerProvider.kt
new file mode 100644
index 0000000..6a804bf
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DnsAnswerProvider.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 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.CLASS_IN
+import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.DnsPacket.ANSECTION
+import java.net.InetAddress
+import java.util.concurrent.ConcurrentHashMap
+
+const val DEFAULT_TTL_S = 5L
+
+/**
+ * Helper class to store the mapping of DNS queries.
+ *
+ * DnsAnswerProvider is built atop a ConcurrentHashMap and as such it provides the same
+ * guarantees as ConcurrentHashMap between writing and reading elements. Specifically :
+ * - Setting an answer happens-before reading the same answer.
+ * - Callers can read and write concurrently from DnsAnswerProvider and expect no
+ * ConcurrentModificationException.
+ * Freshness of the answers depends on ordering of the threads ; if callers need a
+ * freshness guarantee, they need to provide the happens-before relationship from a
+ * write that they want to observe to the read that they need to be observed.
+ */
+class DnsAnswerProvider {
+ private val mDnsKeyToRecords = ConcurrentHashMap<String, List<DnsPacket.DnsRecord>>()
+
+ /**
+ * Get answer for the specified hostname.
+ *
+ * @param query the target hostname.
+ * @param type type of record, could be A or AAAA.
+ *
+ * @return list of [DnsPacket.DnsRecord] associated to the query. Empty if no record matches.
+ */
+ fun getAnswer(query: String, type: Int) = mDnsKeyToRecords[query]
+ .orEmpty().filter { it.nsType == type }
+
+ /** Set answer for the specified {@code query}.
+ *
+ * @param query the target hostname
+ * @param addresses [List<InetAddress>] which could be used to generate multiple A or AAAA
+ * RRs with the corresponding addresses.
+ */
+ fun setAnswer(query: String, hosts: List<InetAddress>) = mDnsKeyToRecords.put(query, hosts.map {
+ DnsPacket.DnsRecord.makeAOrAAAARecord(ANSECTION, query, CLASS_IN, DEFAULT_TTL_S, it)
+ })
+
+ fun clearAnswer(query: String) = mDnsKeyToRecords.remove(query)
+ fun clearAll() = mDnsKeyToRecords.clear()
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java
new file mode 100644
index 0000000..d103748
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DumpTestUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 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 static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Utilities for testing output of service dumps.
+ */
+public class DumpTestUtils {
+
+ private static String dumpService(String serviceName, boolean adoptPermission, String... args)
+ throws RemoteException, InterruptedException, ErrnoException {
+ final IBinder ib = ServiceManager.getService(serviceName);
+ FileDescriptor[] pipe = Os.pipe();
+
+ // Start a thread to read the dump output, or dump might block if it fills the pipe.
+ final CountDownLatch latch = new CountDownLatch(1);
+ AtomicReference<String> output = new AtomicReference<>();
+ // Used to send exceptions back to the main thread to ensure that the test fails cleanly.
+ AtomicReference<Exception> exception = new AtomicReference<>();
+ new Thread(() -> {
+ try {
+ output.set(Streams.readFully(
+ new InputStreamReader(new FileInputStream(pipe[0]),
+ StandardCharsets.UTF_8)));
+ latch.countDown();
+ } catch (Exception e) {
+ exception.set(e);
+ latch.countDown();
+ }
+ }).start();
+
+ final int timeoutMs = 5_000;
+ final String what = "service '" + serviceName + "' with args: " + Arrays.toString(args);
+ try {
+ if (adoptPermission) {
+ runAsShell(android.Manifest.permission.DUMP, () -> ib.dump(pipe[1], args));
+ } else {
+ ib.dump(pipe[1], args);
+ }
+ IoUtils.closeQuietly(pipe[1]);
+ assertTrue("Dump of " + what + " timed out after " + timeoutMs + "ms",
+ latch.await(timeoutMs, TimeUnit.MILLISECONDS));
+ } finally {
+ // Closing the fds will terminate the thread if it's blocked on read.
+ IoUtils.closeQuietly(pipe[0]);
+ if (pipe[1].valid()) IoUtils.closeQuietly(pipe[1]);
+ }
+ if (exception.get() != null) {
+ fail("Exception dumping " + what + ": " + exception.get());
+ }
+ return output.get();
+ }
+
+ /**
+ * Dumps the specified service and returns a string. Sends a dump IPC to the given service
+ * with the specified args and a pipe, then reads from the pipe in a separate thread.
+ * The current process must already have the DUMP permission.
+ *
+ * @param serviceName the service to dump.
+ * @param args the arguments to pass to the dump function.
+ * @return The dump text.
+ * @throws RemoteException dumping the service failed.
+ * @throws InterruptedException the dump timed out.
+ * @throws ErrnoException opening or closing the pipe for the dump failed.
+ */
+ public static String dumpService(String serviceName, String... args)
+ throws RemoteException, InterruptedException, ErrnoException {
+ return dumpService(serviceName, false, args);
+ }
+
+ /**
+ * Dumps the specified service and returns a string. Sends a dump IPC to the given service
+ * with the specified args and a pipe, then reads from the pipe in a separate thread.
+ * Adopts the {@code DUMP} permission via {@code adoptShellPermissionIdentity} and then releases
+ * it. This method should not be used if the caller already has the shell permission identity.
+ * TODO: when Q and R are no longer supported, use
+ * {@link android.app.UiAutomation#getAdoptedShellPermissions} to automatically acquire the
+ * shell permission if the caller does not already have it.
+ *
+ * @param serviceName the service to dump.
+ * @param args the arguments to pass to the dump function.
+ * @return The dump text.
+ * @throws RemoteException dumping the service failed.
+ * @throws InterruptedException the dump timed out.
+ * @throws ErrnoException opening or closing the pipe for the dump failed.
+ */
+ public static String dumpServiceWithShellPermission(String serviceName, String... args)
+ throws RemoteException, InterruptedException, ErrnoException {
+ return dumpService(serviceName, true, args);
+ }
+}
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..1f82a35
--- /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 != null && !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..f00ca11
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
@@ -0,0 +1,90 @@
+/*
+ * 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 android.util.Log
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import com.android.testutils.FunctionalUtils.ThrowingSupplier
+import java.lang.Exception
+import java.util.concurrent.Executor
+import kotlin.test.fail
+
+private const val TAG = "HandlerUtils"
+
+/**
+ * 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")
+ }
+}
+
+/**
+ * Executes a block of code that returns a value, making its side effects visible on the caller and
+ * the handler thread.
+ *
+ * After this function returns, the side effects of the passed block of code are guaranteed to be
+ * observed both on the thread running the handler and on the thread running this method.
+ * To achieve this, this method runs the passed block on the handler and blocks this thread
+ * until it's executed, so keep in mind this method will block, (including, if the handler isn't
+ * running, blocking forever).
+ */
+fun <T> visibleOnHandlerThread(handler: Handler, supplier: ThrowingSupplier<T>): T {
+ val cv = ConditionVariable()
+ var rv: Result<T> = Result.failure(RuntimeException("Not run"))
+ handler.post {
+ try {
+ rv = Result.success(supplier.get())
+ } catch (exception: Exception) {
+ Log.e(TAG, "visibleOnHandlerThread caught exception", exception)
+ rv = Result.failure(exception)
+ }
+ cv.open()
+ }
+ // After block() returns, the handler thread has seen the change (since it ran it)
+ // and this thread also has seen the change (since cv.open() happens-before cv.block()
+ // returns).
+ cv.block()
+ return rv.getOrThrow()
+}
+
+/** Overload of visibleOnHandlerThread but executes a block of code that does not return a value. */
+inline fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable){
+ visibleOnHandlerThread(handler, ThrowingSupplier<Unit> { r.run() })
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
new file mode 100644
index 0000000..d7961a0
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the external {@link TestNetworkInterface} to the internal
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatExternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ extAddr: InetAddress,
+ natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+ /**
+ * Rewrite addresses, ports and fix up checksums for packets received on the external
+ * interface.
+ *
+ * Incoming response from external interface which is being forwarded to the internal
+ * interface with translated address, e.g. 1.2.3.4:80 -> 8.8.8.8:1234
+ * will be translated into 8.8.8.8:80 -> 192.168.1.1:5678.
+ *
+ * For packets that are not an incoming response, do not forward them to the
+ * internal interface.
+ */
+ override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+ val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+ // TODO: support one external address per ip version.
+ val extAddrBuf = mExtAddr.address
+ if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+ // Get internal address by port.
+ val transportOffset =
+ if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+ else PacketReflector.IPV6_HEADER_LENGTH
+ val dstPort = getPortAt(buf, transportOffset + DESTINATION_PORT_OFFSET)
+ val intAddrInfo = synchronized(mNatMap) { mNatMap.fromExternalPort(dstPort) }
+ // No mapping, skip. This usually happens if the connection is initiated directly on
+ // the external interface, e.g. DNS64 resolution, network validation, etc.
+ if (intAddrInfo == null) return
+
+ val intAddrBuf = intAddrInfo.address.address
+ val intPort = intAddrInfo.port
+
+ // Copy the original destination to into the source address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + i] = buf[addrPos + addrLen + i]
+ }
+
+ // Copy the internal address into the destination address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + addrLen + i] = intAddrBuf[i]
+ }
+
+ // Copy the internal port into the destination port.
+ setPortAt(intPort, buf, transportOffset + DESTINATION_PORT_OFFSET)
+
+ // Fix IP and Transport layer checksum.
+ fixPacketChecksum(buf, len, version, proto.toByte())
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
new file mode 100644
index 0000000..fa39d19
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the internal {@link TestNetworkInterface} to the external
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatInternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ extAddr: InetAddress,
+ natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+ /**
+ * Rewrite addresses, ports and fix up checksums for packets received on the internal
+ * interface.
+ *
+ * Outgoing packet from the internal interface which is being forwarded to the
+ * external interface with translated address, e.g. 192.168.1.1:5678 -> 8.8.8.8:80
+ * will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * The external port, e.g. 1234 in the above example, is the port number assigned by
+ * the forwarder when creating the mapping to identify the source address and port when
+ * the response is coming from the external interface. See {@link PacketBridge.NatMap}
+ * for detail.
+ */
+ override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+ val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+ // TODO: support one external address per ip version.
+ val extAddrBuf = mExtAddr.address
+ if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+ val srcAddr = getInetAddressAt(buf, addrPos, addrLen)
+
+ // Copy the original destination to into the source address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + i] = buf[addrPos + addrLen + i]
+ }
+
+ // Copy the external address into the destination address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + addrLen + i] = extAddrBuf[i]
+ }
+
+ // Add an entry to NAT mapping table.
+ val transportOffset =
+ if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+ else PacketReflector.IPV6_HEADER_LENGTH
+ val srcPort = getPortAt(buf, transportOffset)
+ val extPort = synchronized(mNatMap) { mNatMap.toExternalPort(srcAddr, srcPort, proto) }
+ // Copy the external port to into the source port.
+ setPortAt(extPort, buf, transportOffset)
+
+ // Fix IP and Transport layer checksum.
+ fixPacketChecksum(buf, len, version, proto.toByte())
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java b/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
new file mode 100644
index 0000000..0a2b5d4
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 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 static com.android.testutils.PacketReflector.IPPROTO_TCP;
+import static com.android.testutils.PacketReflector.IPPROTO_UDP;
+import static com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_PROTO_OFFSET;
+import static com.android.testutils.PacketReflector.TCP_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.UDP_HEADER_LENGTH;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Objects;
+
+/**
+ * A class that forwards packets from a {@link TestNetworkInterface} to another
+ * {@link TestNetworkInterface} with NAT.
+ *
+ * For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
+ * which allows content injection on the test network. However, this could be hard to use
+ * because the callers need to compose IP packets in order to inject content to the
+ * test network.
+ *
+ * In order to remove the need of composing the IP packets, this class forwards IP packets to
+ * the {@link FileDescriptor} of another {@link TestNetworkInterface} instance. Thus,
+ * the TCP/IP headers could be parsed/composed automatically by the protocol stack of this
+ * additional {@link TestNetworkInterface}, while the payload is supplied by the
+ * servers run on the interface.
+ *
+ * To make it work, an internal interface and an external interface are defined, where
+ * the client might send packets from the internal interface which are originated from
+ * multiple addresses to a server that listens on the external address.
+ *
+ * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
+ * is implemented during forwarding, which will swap the source and destination,
+ * but replacing the source address with the external address,
+ * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * For the above example, a client who sends http request will have a hallucination that
+ * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
+ * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
+ * to a local address 1.2.3.4.
+ *
+ * And a NAT mapping is created at the time when the outgoing packet is forwarded.
+ * With a different internal source port, the instance learned that when a response with the
+ * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
+ *
+ * For the incoming packet received from external interface, for example a http response sent
+ * from the http server, the same mechanism is applied but in a different direction,
+ * where the source and destination will be swapped, and the source address will be replaced
+ * with the internal address, which is obtained from the NAT mapping described above.
+ */
+public abstract class NatPacketForwarderBase extends Thread {
+ private static final String TAG = "NatPacketForwarder";
+ static final int DESTINATION_PORT_OFFSET = 2;
+
+ // The source fd to read packets from.
+ @NonNull
+ final FileDescriptor mSrcFd;
+ // The buffer to temporarily hold the entire packet after receiving.
+ @NonNull
+ final byte[] mBuf;
+ // The destination fd to write packets to.
+ @NonNull
+ final FileDescriptor mDstFd;
+ // The NAT mapping table shared between two NatPacketForwarder instances to map from
+ // the source port to the associated internal address. The map can be read/write from two
+ // different threads on any given time whenever receiving packets on the
+ // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
+ @GuardedBy("mNatMap")
+ @NonNull
+ final PacketBridge.NatMap mNatMap;
+ // The address of the external interface. See {@link NatPacketForwarder}.
+ @NonNull
+ final InetAddress mExtAddr;
+
+ /**
+ * Construct a {@link NatPacketForwarderBase}.
+ *
+ * This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
+ * forwards them to the {@code dstFd} of another {@link TestNetworkInterface} with
+ * NAT applied. See {@link NatPacketForwarderBase}.
+ *
+ * To apply NAT, the address of the external interface needs to be supplied through
+ * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
+ * {@code natMap} is needed to be shared between these two instances.
+ *
+ * Note that this class is not useful if the instance is not managed by a
+ * {@link PacketBridge} to set up a two-way communication.
+ *
+ * @param srcFd {@link FileDescriptor} to read packets from.
+ * @param mtu MTU of the test network.
+ * @param dstFd {@link FileDescriptor} to write packets to.
+ * @param extAddr the external address, which is the address of the external interface.
+ * See {@link NatPacketForwarderBase}.
+ * @param natMap the NAT mapping table shared between two {@link NatPacketForwarderBase}
+ * instance.
+ */
+ public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
+ @NonNull PacketBridge.NatMap natMap) {
+ super(TAG);
+ mSrcFd = Objects.requireNonNull(srcFd);
+ mBuf = new byte[mtu];
+ mDstFd = Objects.requireNonNull(dstFd);
+ mExtAddr = Objects.requireNonNull(extAddr);
+ mNatMap = Objects.requireNonNull(natMap);
+ }
+
+ /**
+ * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
+ * which includes re-write addresses, ports and fix up checksums.
+ * Subclasses should override this method to implement a simple NAT.
+ */
+ abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
+
+ private void forwardPacket(@NonNull byte[] buf, int len) {
+ try {
+ Os.write(mDstFd, buf, 0, len);
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "Error writing packet: " + e.getMessage());
+ }
+ }
+
+ // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+ private void processPacket() {
+ final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
+ if (len < 1) {
+ // Usually happens when socket read is being interrupted, e.g. stopping PacketForwarder.
+ return;
+ }
+
+ final int version = mBuf[0] >>> 4;
+ final int protoPos, ipHdrLen;
+ switch (version) {
+ case 4:
+ ipHdrLen = IPV4_HEADER_LENGTH;
+ protoPos = PacketReflector.IPV4_PROTO_OFFSET;
+ break;
+ case 6:
+ ipHdrLen = IPV6_HEADER_LENGTH;
+ protoPos = IPV6_PROTO_OFFSET;
+ break;
+ default:
+ throw new IllegalStateException("Unexpected version: " + version);
+ }
+ if (len < ipHdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ final byte proto = mBuf[protoPos];
+ final int transportHdrLen;
+ switch (proto) {
+ case IPPROTO_TCP:
+ transportHdrLen = TCP_HEADER_LENGTH;
+ break;
+ case IPPROTO_UDP:
+ transportHdrLen = UDP_HEADER_LENGTH;
+ break;
+ // TODO: Support ICMP.
+ default:
+ return; // Unknown protocol, ignored.
+ }
+
+ if (len < ipHdrLen + transportHdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+ // Re-write addresses, ports and fix up checksums.
+ preparePacketForForwarding(mBuf, len, version, proto);
+ // Send the packet to the destination fd.
+ forwardPacket(mBuf, len);
+ }
+
+ @Override
+ public void run() {
+ Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+ while (!interrupted() && mSrcFd.valid()) {
+ processPacket();
+ }
+ Log.i(TAG, "exiting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetlinkTestUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetlinkTestUtils.kt
new file mode 100644
index 0000000..3f5460b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetlinkTestUtils.kt
@@ -0,0 +1,108 @@
+/*
+ * 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("NetlinkTestUtils")
+
+package com.android.testutils
+
+import com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH
+import com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH
+import libcore.util.HexEncoding
+import libcore.util.HexEncoding.encodeToString
+import java.net.Inet6Address
+import java.net.InetAddress
+
+private const val VRRP_MAC_ADDR = "00005e000164"
+
+/**
+ * Make a RTM_NEWNEIGH netlink message.
+ */
+@JvmOverloads
+fun makeNewNeighMessage(
+ neighAddr: InetAddress,
+ nudState: Short,
+ linkLayerAddr: String = VRRP_MAC_ADDR
+) = makeNeighborMessage(
+ neighAddr = neighAddr,
+ type = RTM_NEWNEIGH,
+ nudState = nudState,
+ linkLayerAddr = linkLayerAddr
+)
+
+/**
+ * Make a RTM_DELNEIGH netlink message.
+ */
+fun makeDelNeighMessage(
+ neighAddr: InetAddress,
+ nudState: Short
+) = makeNeighborMessage(
+ neighAddr = neighAddr,
+ type = RTM_DELNEIGH,
+ nudState = nudState
+)
+
+private fun makeNeighborMessage(
+ neighAddr: InetAddress,
+ type: Short,
+ nudState: Short,
+ linkLayerAddr: String = VRRP_MAC_ADDR
+) = HexEncoding.decode(
+ /* ktlint-disable indent */
+ // -- struct nlmsghdr --
+ // length = 88 or 76:
+ (if (neighAddr is Inet6Address) "58000000" else "4c000000") +
+ type.toLEHex() + // type
+ "0000" + // flags
+ "00000000" + // seqno
+ "00000000" + // pid (0 == kernel)
+ // struct ndmsg
+ // family (AF_INET6 or AF_INET)
+ (if (neighAddr is Inet6Address) "0a" else "02") +
+ "00" + // pad1
+ "0000" + // pad2
+ "15000000" + // interface index (21 == wlan0, on test device)
+ nudState.toLEHex() + // NUD state
+ "00" + // flags
+ "01" + // type
+ // -- struct nlattr: NDA_DST --
+ // length = 20 or 8:
+ (if (neighAddr is Inet6Address) "1400" else "0800") +
+ "0100" + // type (1 == NDA_DST, for neighbor messages)
+ // IP address:
+ encodeToString(neighAddr.address) +
+ // -- struct nlattr: NDA_LLADDR --
+ "0a00" + // length = 10
+ "0200" + // type (2 == NDA_LLADDR, for neighbor messages)
+ linkLayerAddr + // MAC Address(default == 00:00:5e:00:01:64)
+ "0000" + // padding, for 4 byte alignment
+ // -- struct nlattr: NDA_PROBES --
+ "0800" + // length = 8
+ "0400" + // type (4 == NDA_PROBES, for neighbor messages)
+ "01000000" + // number of probes
+ // -- struct nlattr: NDA_CACHEINFO --
+ "1400" + // length = 20
+ "0300" + // type (3 == NDA_CACHEINFO, for neighbor messages)
+ "05190000" + // ndm_used, as "clock ticks ago"
+ "05190000" + // ndm_confirmed, as "clock ticks ago"
+ "190d0000" + // ndm_updated, as "clock ticks ago"
+ "00000000", // ndm_refcnt
+ false /* allowSingleChar */)
+ /* ktlint-enable indent */
+
+/**
+ * Convert a [Short] to a little-endian hex string.
+ */
+private fun Short.toLEHex() = String.format("%04x", java.lang.Short.reverseBytes(this))
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..642da7a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsProviderCbStubCompat.java
@@ -0,0 +1,46 @@
+/*
+ * 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 {}
+
+ /** Added in T. */
+ public void notifyLimitReached() throws RemoteException {}
+
+ /** Added in T. */
+ public void notifyWarningReached() throws RemoteException {}
+
+ /** Added in S, removed in T. */
+ 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/NonNullTestUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/NonNullTestUtils.java
new file mode 100644
index 0000000..463c470
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NonNullTestUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 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.annotation.NonNull;
+
+/**
+ * Utilities to help Kotlin test to verify java @NonNull code
+ */
+public class NonNullTestUtils {
+ /**
+ * This method allows Kotlin to pass nullable to @NonNull java code for testing.
+ * For Foo(@NonNull arg) java method, Kotlin can pass nullable variable by
+ * Foo(NonNullTestUtils.nullUnsafe(nullableVar)).
+ */
+ @NonNull public static <T> T nullUnsafe(T v) {
+ return v;
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
new file mode 100644
index 0000000..d50f78a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 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.LinkAddress
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.TestNetworkSpecifier
+import android.os.Binder
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import java.net.InetAddress
+import libcore.io.IoUtils
+
+private const val MIN_PORT_NUMBER = 1025
+private const val MAX_PORT_NUMBER = 65535
+
+/**
+ * A class that set up two {@link TestNetworkInterface} with NAT, and forward packets between them.
+ *
+ * See {@link NatPacketForwarder} for more detailed information.
+ */
+class PacketBridge(
+ context: Context,
+ internalAddr: LinkAddress,
+ externalAddr: LinkAddress,
+ dnsAddr: InetAddress
+) {
+ private val natMap = NatMap()
+ private val binder = Binder()
+
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val tnm = context.getSystemService(TestNetworkManager::class.java)!!
+
+ // Create test networks.
+ private val internalIface = tnm.createTunInterface(listOf(internalAddr))
+ private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+
+ // Register test networks to ConnectivityService.
+ private val internalNetworkCallback: TestableNetworkCallback
+ private val externalNetworkCallback: TestableNetworkCallback
+ val internalNetwork: Network
+ val externalNetwork: Network
+ init {
+ val (inCb, inNet) = createTestNetwork(internalIface, internalAddr, dnsAddr)
+ val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
+ internalNetworkCallback = inCb
+ externalNetworkCallback = exCb
+ internalNetwork = inNet
+ externalNetwork = exNet
+ }
+
+ // Setup the packet bridge.
+ private val internalFd = internalIface.fileDescriptor.fileDescriptor
+ private val externalFd = externalIface.fileDescriptor.fileDescriptor
+
+ private val pr1 = NatInternalPacketForwarder(
+ internalFd,
+ 1500,
+ externalFd,
+ externalAddr.address,
+ natMap
+ )
+ private val pr2 = NatExternalPacketForwarder(
+ externalFd,
+ 1500,
+ internalFd,
+ externalAddr.address,
+ natMap
+ )
+
+ fun start() {
+ IoUtils.setBlocking(internalFd, true /* blocking */)
+ IoUtils.setBlocking(externalFd, true /* blocking */)
+ pr1.start()
+ pr2.start()
+ }
+
+ fun stop() {
+ pr1.interrupt()
+ pr2.interrupt()
+ cm.unregisterNetworkCallback(internalNetworkCallback)
+ cm.unregisterNetworkCallback(externalNetworkCallback)
+ }
+
+ /**
+ * Creates a test network with given test TUN interface and addresses.
+ */
+ private fun createTestNetwork(
+ testIface: TestNetworkInterface,
+ addr: LinkAddress,
+ dnsAddr: InetAddress
+ ): Pair<TestableNetworkCallback, Network> {
+ // Make a network request to hold the test network
+ val nr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(TestNetworkSpecifier(testIface.interfaceName))
+ .build()
+ val testCb = TestableNetworkCallback()
+ cm.requestNetwork(nr, testCb)
+
+ val lp = LinkProperties().apply {
+ addLinkAddress(addr)
+ interfaceName = testIface.interfaceName
+ addDnsServer(dnsAddr)
+ }
+ tnm.setupTestNetwork(lp, true /* isMetered */, binder)
+
+ // Wait for available before return.
+ val network = testCb.expect<Available>().network
+ return testCb to network
+ }
+
+ /**
+ * A helper class to maintain the mappings between internal addresses/ports and external
+ * ports.
+ *
+ * This class assigns an unused external port number if the mapping between
+ * srcaddress:srcport:protocol and the external port does not exist yet.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+ class NatMap {
+ data class AddressInfo(val address: InetAddress, val port: Int, val protocol: Int)
+
+ private val mToExternalPort = HashMap<AddressInfo, Int>()
+ private val mFromExternalPort = HashMap<Int, AddressInfo>()
+
+ // Skip well-known port 0~1024.
+ private var nextExternalPort = MIN_PORT_NUMBER
+
+ fun toExternalPort(addr: InetAddress, port: Int, protocol: Int): Int {
+ val info = AddressInfo(addr, port, protocol)
+ val extPort: Int
+ if (!mToExternalPort.containsKey(info)) {
+ extPort = nextExternalPort++
+ if (nextExternalPort > MAX_PORT_NUMBER) {
+ throw IllegalStateException("Available ports are exhausted")
+ }
+ mToExternalPort[info] = extPort
+ mFromExternalPort[extPort] = info
+ } else {
+ extPort = mToExternalPort[info]!!
+ }
+ return extPort
+ }
+
+ fun fromExternalPort(port: Int): AddressInfo? {
+ return mFromExternalPort[port]
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
new file mode 100644
index 0000000..69392d4
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2014 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 static android.system.OsConstants.ICMP6_ECHO_REPLY;
+import static android.system.OsConstants.ICMP6_ECHO_REQUEST;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * A class that echoes packets received on a {@link TestNetworkInterface} back to itself.
+ *
+ * For testing purposes, sometimes a mocked environment to simulate a simple echo from the
+ * server side is needed. This is particularly useful if the test, e.g. VpnTest, is
+ * heavily relying on the outside world.
+ *
+ * This class reads packets from the {@link FileDescriptor} of a {@link TestNetworkInterface}, and:
+ * 1. For TCP and UDP packets, simply swaps the source address and the destination
+ * address, then send it back to the {@link FileDescriptor}.
+ * 2. For ICMP ping packets, composes a ping reply and sends it back to the sender.
+ * 3. Ignore all other packets.
+ */
+public class PacketReflector extends Thread {
+
+ static final int IPV4_HEADER_LENGTH = 20;
+ static final int IPV6_HEADER_LENGTH = 40;
+
+ static final int IPV4_ADDR_OFFSET = 12;
+ static final int IPV6_ADDR_OFFSET = 8;
+ static final int IPV4_ADDR_LENGTH = 4;
+ static final int IPV6_ADDR_LENGTH = 16;
+
+ static final int IPV4_PROTO_OFFSET = 9;
+ static final int IPV6_PROTO_OFFSET = 6;
+
+ static final byte IPPROTO_ICMP = 1;
+ static final byte IPPROTO_TCP = 6;
+ static final byte IPPROTO_UDP = 17;
+ private static final byte IPPROTO_ICMPV6 = 58;
+
+ private static final int ICMP_HEADER_LENGTH = 8;
+ static final int TCP_HEADER_LENGTH = 20;
+ static final int UDP_HEADER_LENGTH = 8;
+
+ private static final byte ICMP_ECHO = 8;
+ private static final byte ICMP_ECHOREPLY = 0;
+
+ private static String TAG = "PacketReflector";
+
+ @NonNull
+ private final FileDescriptor mFd;
+ @NonNull
+ private final byte[] mBuf;
+
+ /**
+ * Construct a {@link PacketReflector} from the given {@code fd} of
+ * a {@link TestNetworkInterface}.
+ *
+ * @param fd {@link FileDescriptor} to read/write packets.
+ * @param mtu MTU of the test network.
+ */
+ public PacketReflector(@NonNull FileDescriptor fd, int mtu) {
+ super("PacketReflector");
+ mFd = Objects.requireNonNull(fd);
+ mBuf = new byte[mtu];
+ }
+
+ private static void swapBytes(@NonNull byte[] buf, int pos1, int pos2, int len) {
+ for (int i = 0; i < len; i++) {
+ byte b = buf[pos1 + i];
+ buf[pos1 + i] = buf[pos2 + i];
+ buf[pos2 + i] = b;
+ }
+ }
+
+ private static void swapAddresses(@NonNull byte[] buf, int version) {
+ int addrPos, addrLen;
+ switch (version) {
+ case 4:
+ addrPos = IPV4_ADDR_OFFSET;
+ addrLen = IPV4_ADDR_LENGTH;
+ break;
+ case 6:
+ addrPos = IPV6_ADDR_OFFSET;
+ addrLen = IPV6_ADDR_LENGTH;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
+ }
+
+ // Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
+ // This is used by the test to "connect to itself" through the VPN.
+ private void processTcpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + TCP_HEADER_LENGTH) {
+ return;
+ }
+
+ // Swap src and dst IP addresses.
+ swapAddresses(buf, version);
+
+ // Send the packet back.
+ writePacket(buf, len);
+ }
+
+ // Echo UDP packets: swap source and destination addresses, and source and destination ports.
+ // This is used by the test to check that the bytes it sends are echoed back.
+ private void processUdpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + UDP_HEADER_LENGTH) {
+ return;
+ }
+
+ // Swap src and dst IP addresses.
+ swapAddresses(buf, version);
+
+ // Swap dst and src ports.
+ int portOffset = hdrLen;
+ swapBytes(buf, portOffset, portOffset + 2, 2);
+
+ // Send the packet back.
+ writePacket(buf, len);
+ }
+
+ private void processIcmpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + ICMP_HEADER_LENGTH) {
+ return;
+ }
+
+ byte type = buf[hdrLen];
+ if (!(version == 4 && type == ICMP_ECHO) &&
+ !(version == 6 && type == (byte) ICMP6_ECHO_REQUEST)) {
+ return;
+ }
+
+ // Save the ping packet we received.
+ byte[] request = buf.clone();
+
+ // Swap src and dst IP addresses, and send the packet back.
+ // This effectively pings the device to see if it replies.
+ swapAddresses(buf, version);
+ writePacket(buf, len);
+
+ // The device should have replied, and buf should now contain a ping response.
+ int received = PacketReflectorUtil.readPacket(mFd, buf);
+ if (received != len) {
+ Log.i(TAG, "Reflecting ping did not result in ping response: " +
+ "read=" + received + " expected=" + len);
+ return;
+ }
+
+ byte replyType = buf[hdrLen];
+ if ((type == ICMP_ECHO && replyType != ICMP_ECHOREPLY)
+ || (type == (byte) ICMP6_ECHO_REQUEST && replyType != (byte) ICMP6_ECHO_REPLY)) {
+ Log.i(TAG, "Received unexpected ICMP reply: original " + type
+ + ", reply " + replyType);
+ return;
+ }
+
+ // Compare the response we got with the original packet.
+ // The only thing that should have changed are addresses, type and checksum.
+ // Overwrite them with the received bytes and see if the packet is otherwise identical.
+ request[hdrLen] = buf[hdrLen]; // Type
+ request[hdrLen + 2] = buf[hdrLen + 2]; // Checksum byte 1.
+ request[hdrLen + 3] = buf[hdrLen + 3]; // Checksum byte 2.
+
+ // Since Linux kernel 4.2, net.ipv6.auto_flowlabels is set by default, and therefore
+ // the request and reply may have different IPv6 flow label: ignore that as well.
+ if (version == 6) {
+ request[1] = (byte) (request[1] & 0xf0 | buf[1] & 0x0f);
+ request[2] = buf[2];
+ request[3] = buf[3];
+ }
+
+ for (int i = 0; i < len; i++) {
+ if (buf[i] != request[i]) {
+ Log.i(TAG, "Received non-matching packet when expecting ping response.");
+ return;
+ }
+ }
+
+ // Now swap the addresses again and reflect the packet. This sends a ping reply.
+ swapAddresses(buf, version);
+ writePacket(buf, len);
+ }
+
+ private void writePacket(@NonNull byte[] buf, int len) {
+ try {
+ Os.write(mFd, buf, 0, len);
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "Error writing packet: " + e.getMessage());
+ }
+ }
+
+ // Reads one packet from our mFd, and possibly writes the packet back.
+ private void processPacket() {
+ int len = PacketReflectorUtil.readPacket(mFd, mBuf);
+ if (len < 1) {
+ // Usually happens when socket read is being interrupted, e.g. stopping PacketReflector.
+ return;
+ }
+
+ int version = mBuf[0] >> 4;
+ int protoPos, hdrLen;
+ if (version == 4) {
+ hdrLen = IPV4_HEADER_LENGTH;
+ protoPos = IPV4_PROTO_OFFSET;
+ } else if (version == 6) {
+ hdrLen = IPV6_HEADER_LENGTH;
+ protoPos = IPV6_PROTO_OFFSET;
+ } else {
+ throw new IllegalStateException("Unexpected version: " + version);
+ }
+
+ if (len < hdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ byte proto = mBuf[protoPos];
+ switch (proto) {
+ case IPPROTO_ICMP:
+ // fall through
+ case IPPROTO_ICMPV6:
+ processIcmpPacket(mBuf, version, len, hdrLen);
+ break;
+ case IPPROTO_TCP:
+ processTcpPacket(mBuf, version, len, hdrLen);
+ break;
+ case IPPROTO_UDP:
+ processUdpPacket(mBuf, version, len, hdrLen);
+ break;
+ }
+ }
+
+ public void run() {
+ Log.i(TAG, "starting fd=" + mFd + " valid=" + mFd.valid());
+ while (!interrupted() && mFd.valid()) {
+ processPacket();
+ }
+ Log.i(TAG, "exiting fd=" + mFd + " valid=" + mFd.valid());
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
new file mode 100644
index 0000000..498b1a3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 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("PacketReflectorUtil")
+
+package com.android.testutils
+
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants
+import com.android.net.module.util.IpUtils
+import com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.io.FileDescriptor
+import java.io.InterruptedIOException
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+fun readPacket(fd: FileDescriptor, buf: ByteArray): Int {
+ return try {
+ Os.read(fd, buf, 0, buf.size)
+ } catch (e: ErrnoException) {
+ // Ignore normal use cases such as the EAGAIN error indicates that the read operation
+ // cannot be completed immediately, or the EINTR error indicates that the read
+ // operation was interrupted by a signal.
+ if (e.errno == OsConstants.EAGAIN || e.errno == OsConstants.EINTR) {
+ -1
+ } else {
+ throw e
+ }
+ } catch (e: InterruptedIOException) {
+ -1
+ }
+}
+
+fun getInetAddressAt(buf: ByteArray, pos: Int, len: Int): InetAddress =
+ InetAddress.getByAddress(buf.copyOfRange(pos, pos + len))
+
+/**
+ * Reads a 16-bit unsigned int at pos in big endian, with no alignment requirements.
+ */
+fun getPortAt(buf: ByteArray, pos: Int): Int {
+ return (buf[pos].toInt() and 0xff shl 8) + (buf[pos + 1].toInt() and 0xff)
+}
+
+fun setPortAt(port: Int, buf: ByteArray, pos: Int) {
+ buf[pos] = (port ushr 8).toByte()
+ buf[pos + 1] = (port and 0xff).toByte()
+}
+
+fun getAddressPositionAndLength(version: Int) = when (version) {
+ 4 -> PacketReflector.IPV4_ADDR_OFFSET to PacketReflector.IPV4_ADDR_LENGTH
+ 6 -> PacketReflector.IPV6_ADDR_OFFSET to PacketReflector.IPV6_ADDR_LENGTH
+ else -> throw IllegalArgumentException("Unknown IP version $version")
+}
+
+private const val IPV4_CHKSUM_OFFSET = 10
+private const val UDP_CHECKSUM_OFFSET = 6
+private const val TCP_CHECKSUM_OFFSET = 16
+
+fun fixPacketChecksum(buf: ByteArray, len: Int, version: Int, protocol: Byte) {
+ // Fill Ip checksum for IPv4. IPv6 header doesn't have a checksum field.
+ if (version == 4) {
+ val checksum = IpUtils.ipChecksum(ByteBuffer.wrap(buf), 0)
+ // Place checksum in Big-endian order.
+ buf[IPV4_CHKSUM_OFFSET] = (checksum.toInt() ushr 8).toByte()
+ buf[IPV4_CHKSUM_OFFSET + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+
+ // Fill transport layer checksum.
+ val transportOffset = if (version == 4) IPV4_HEADER_LENGTH else IPV6_HEADER_LENGTH
+ when (protocol) {
+ PacketReflector.IPPROTO_UDP -> {
+ val checksumPos = transportOffset + UDP_CHECKSUM_OFFSET
+ // Clear before calculate.
+ buf[checksumPos + 1] = 0x00
+ buf[checksumPos] = buf[checksumPos + 1]
+ val checksum = IpUtils.udpChecksum(
+ ByteBuffer.wrap(buf), 0,
+ transportOffset
+ )
+ buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+ buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+ PacketReflector.IPPROTO_TCP -> {
+ val checksumPos = transportOffset + TCP_CHECKSUM_OFFSET
+ // Clear before calculate.
+ buf[checksumPos + 1] = 0x00
+ buf[checksumPos] = buf[checksumPos + 1]
+ val transportLen: Int = len - transportOffset
+ val checksum = IpUtils.tcpChecksum(
+ ByteBuffer.wrap(buf), 0, transportOffset,
+ transportLen
+ )
+ buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+ buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+ // TODO: Support ICMP.
+ else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
+ }
+}
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..964c6c6
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketResponder.kt
@@ -0,0 +1,75 @@
+/*
+ * 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()
+ replyThread.join()
+ }
+
+ 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/RouterAdvertisementResponder.java b/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
new file mode 100644
index 0000000..51d57bc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/RouterAdvertisementResponder.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 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 static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.NsHeader;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RdnssOption;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+
+/**
+ * ND (RA & NA) responder class useful for tests that require a provisioned IPv6 interface.
+ * TODO: rename to NdResponder
+ */
+public class RouterAdvertisementResponder extends PacketResponder {
+ private static final String TAG = "RouterAdvertisementResponder";
+ private static final Inet6Address DNS_SERVER =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:4860:4860::64");
+ private final TapPacketReader mPacketReader;
+ // Maps IPv6 address to MacAddress and isRouter boolean.
+ private final Map<Inet6Address, Pair<MacAddress, Boolean>> mNeighborMap = new ArrayMap<>();
+ private final IpPrefix mPrefix;
+
+ public RouterAdvertisementResponder(TapPacketReader packetReader, IpPrefix prefix) {
+ super(packetReader, RouterAdvertisementResponder::isRsOrNs, TAG);
+ mPacketReader = packetReader;
+ mPrefix = Objects.requireNonNull(prefix);
+ }
+
+ public RouterAdvertisementResponder(TapPacketReader packetReader) {
+ this(packetReader, makeRandomPrefix());
+ }
+
+ private static IpPrefix makeRandomPrefix() {
+ final byte[] prefixBytes = new IpPrefix("2001:db8::/64").getAddress().getAddress();
+ final Random r = new Random();
+ for (int i = 4; i < 8; i++) {
+ prefixBytes[i] = (byte) r.nextInt();
+ }
+ return new IpPrefix(prefixBytes, 64);
+ }
+
+ /** Returns true if the packet is a router solicitation or neighbor solicitation message. */
+ private static boolean isRsOrNs(byte[] packet) {
+ final ByteBuffer buffer = ByteBuffer.wrap(packet);
+ final EthernetHeader ethHeader = Struct.parse(EthernetHeader.class, buffer);
+ if (ethHeader.etherType != ETHER_TYPE_IPV6) {
+ return false;
+ }
+ final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buffer);
+ if (ipv6Header.nextHeader != IPPROTO_ICMPV6) {
+ return false;
+ }
+ final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buffer);
+ return icmpv6Header.type == ICMPV6_ROUTER_SOLICITATION
+ || icmpv6Header.type == ICMPV6_NEIGHBOR_SOLICITATION;
+ }
+
+ /**
+ * Adds a new router to be advertised.
+ * @param mac the mac address of the router.
+ * @param ip the link-local address of the router.
+ */
+ public void addRouterEntry(MacAddress mac, Inet6Address ip) {
+ mNeighborMap.put(ip, new Pair<>(mac, true));
+ }
+
+ /**
+ * Adds a new neighbor to be advertised.
+ * @param mac the mac address of the neighbor.
+ * @param ip the link-local address of the neighbor.
+ */
+ public void addNeighborEntry(MacAddress mac, Inet6Address ip) {
+ mNeighborMap.put(ip, new Pair<>(mac, false));
+ }
+
+ /**
+ * @return the prefix that is announced in the Router Advertisements sent by this object.
+ */
+ public IpPrefix getPrefix() {
+ return mPrefix;
+ }
+
+ private ByteBuffer buildPrefixOption() {
+ return PrefixInformationOption.build(
+ mPrefix, (byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS),
+ 3600 /* valid lifetime */, 3600 /* preferred lifetime */);
+ }
+
+ private ByteBuffer buildRdnssOption() {
+ return RdnssOption.build(3600/*lifetime, must be at least 120*/, DNS_SERVER);
+ }
+
+ private ByteBuffer buildSllaOption(MacAddress srcMac) {
+ return LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+ }
+
+ private ByteBuffer buildRaPacket(MacAddress srcMac, MacAddress dstMac, Inet6Address srcIp) {
+ return Ipv6Utils.buildRaPacket(srcMac, dstMac, srcIp, IPV6_ADDR_ALL_NODES_MULTICAST,
+ (byte) 0 /*M=0, O=0*/, 3600 /*lifetime*/, 0 /*reachableTime, unspecified*/,
+ 0/*retransTimer, unspecified*/, buildPrefixOption(), buildRdnssOption(),
+ buildSllaOption(srcMac));
+ }
+
+ private static void sendResponse(TapPacketReader reader, ByteBuffer buffer) {
+ try {
+ reader.sendResponse(buffer);
+ } catch (IOException e) {
+ // Throwing an exception here will crash the test process. Let's stick to logging, as
+ // the test will fail either way.
+ Log.e(TAG, "Failed to send buffer", e);
+ }
+ }
+
+ private void replyToRouterSolicitation(TapPacketReader reader, MacAddress dstMac) {
+ for (Map.Entry<Inet6Address, Pair<MacAddress, Boolean>> it : mNeighborMap.entrySet()) {
+ final boolean isRouter = it.getValue().second;
+ if (!isRouter) {
+ continue;
+ }
+ final ByteBuffer raResponse = buildRaPacket(it.getValue().first, dstMac, it.getKey());
+ sendResponse(reader, raResponse);
+ }
+ }
+
+ private void replyToNeighborSolicitation(TapPacketReader reader, MacAddress dstMac,
+ Inet6Address dstIp, Inet6Address targetIp) {
+ final Pair<MacAddress, Boolean> neighbor = mNeighborMap.get(targetIp);
+ if (neighbor == null) {
+ return;
+ }
+
+ final MacAddress srcMac = neighbor.first;
+ final boolean isRouter = neighbor.second;
+ int flags = NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+ if (isRouter) {
+ flags |= NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER;
+ }
+
+ final ByteBuffer tlla = LlaOption.build((byte) ICMPV6_ND_OPTION_TLLA, srcMac);
+ final ByteBuffer naResponse = Ipv6Utils.buildNaPacket(srcMac, dstMac, targetIp, dstIp,
+ flags, targetIp, tlla);
+ sendResponse(reader, naResponse);
+ }
+
+ @Override
+ protected void replyToPacket(byte[] packet, TapPacketReader reader) {
+ final ByteBuffer buf = ByteBuffer.wrap(packet);
+ // Messages are filtered by parent class, so it is safe to assume that packet is either an
+ // RS or NS.
+ final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+ final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+ final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
+
+ if (icmpv6Header.type == ICMPV6_ROUTER_SOLICITATION) {
+ replyToRouterSolicitation(reader, ethHdr.srcMac);
+ } else if (icmpv6Header.type == ICMPV6_NEIGHBOR_SOLICITATION) {
+ final NsHeader nsHeader = Struct.parse(NsHeader.class, buf);
+ replyToNeighborSolicitation(reader, ethHdr.srcMac, ipv6Hdr.srcIp, nsHeader.target);
+ }
+ }
+}
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/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
new file mode 100644
index 0000000..733bd98
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2022 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.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *
+ * Fake BPF map class for tests that have no privilege to access real BPF maps. TestBpfMap does not
+ * load JNI and all member functions do not access real BPF maps.
+ *
+ * Implements IBpfMap so that any class using IBpfMap can use this class in its tests.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+public class TestBpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V> {
+ private final ConcurrentHashMap<K, V> mMap = new ConcurrentHashMap<>();
+
+ // TODO: Remove this constructor
+ public TestBpfMap(final Class<K> key, final Class<V> value) {
+ }
+
+ @Override
+ public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
+ // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to
+ // implement the entry deletion in the iteration if required.
+ for (Map.Entry<K, V> entry : mMap.entrySet()) {
+ action.accept(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public void updateEntry(K key, V value) throws ErrnoException {
+ mMap.put(key, value);
+ }
+
+ @Override
+ public void insertEntry(K key, V value) throws ErrnoException,
+ IllegalArgumentException {
+ // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry.
+ if (mMap.get(key) != null) {
+ throw new IllegalArgumentException(key + " already exist");
+ }
+ mMap.put(key, value);
+ }
+
+ @Override
+ public void replaceEntry(K key, V value) throws ErrnoException, NoSuchElementException {
+ if (!mMap.containsKey(key)) throw new NoSuchElementException();
+ mMap.put(key, value);
+ }
+
+ @Override
+ public boolean insertOrReplaceEntry(K key, V value) throws ErrnoException {
+ // Returns true if inserted, false if replaced.
+ boolean ret = !mMap.containsKey(key);
+ mMap.put(key, value);
+ return ret;
+ }
+
+ @Override
+ public boolean deleteEntry(Struct key) throws ErrnoException {
+ return mMap.remove(key) != null;
+ }
+
+ @Override
+ public boolean isEmpty() throws ErrnoException {
+ return mMap.isEmpty();
+ }
+
+ @Override
+ public K getNextKey(@NonNull K key) {
+ // Expensive, but since this is only for tests...
+ Iterator<K> it = mMap.keySet().iterator();
+ while (it.hasNext()) {
+ if (Objects.equals(it.next(), key)) {
+ return it.hasNext() ? it.next() : null;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public K getFirstKey() {
+ for (K key : mMap.keySet()) {
+ return key;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean containsKey(@NonNull K key) throws ErrnoException {
+ return mMap.containsKey(key);
+ }
+
+ @Override
+ public V getValue(@NonNull K key) throws ErrnoException {
+ // Return value for a given key. Otherwise, return null without an error ENOENT.
+ // BpfMap#getValue treats that the entry is not found as no error.
+ return mMap.get(key);
+ }
+
+ @Override
+ public void clear() throws ErrnoException {
+ // TODO: consider using mocked #getFirstKey and #deleteEntry to implement.
+ mMap.clear();
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestDnsServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestDnsServer.kt
new file mode 100644
index 0000000..e1b771b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestDnsServer.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2022 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.Network
+import android.util.Log
+import com.android.internal.annotations.GuardedBy
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE
+import com.android.net.module.util.DnsPacket
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.SocketAddress
+import java.net.SocketException
+import java.util.ArrayList
+
+private const val TAG = "TestDnsServer"
+private const val VDBG = true
+@VisibleForTesting(visibility = PRIVATE)
+const val MAX_BUF_SIZE = 8192
+
+/**
+ * A simple implementation of Dns Server that can be bound on specific address and Network.
+ *
+ * The caller should use start() to make the server start a new thread to receive DNS queries
+ * on the bound address, [isAlive] to check status, and stop() for stopping.
+ * The server allows user to manipulate the records to be answered through
+ * [setAnswer] at runtime.
+ *
+ * This server runs on its own thread. Please make sure writing the query to the socket
+ * happens-after using [setAnswer] to guarantee the correct answer is returned. If possible,
+ * use [setAnswer] before calling [start] for simplicity.
+ */
+class TestDnsServer(network: Network, addr: InetSocketAddress) {
+ enum class Status {
+ NOT_STARTED, STARTED, STOPPED
+ }
+ @GuardedBy("thread")
+ private var status: Status = Status.NOT_STARTED
+ private val thread = ReceivingThread()
+ private val socket = DatagramSocket(addr).also { network.bindSocket(it) }
+ private val ansProvider = DnsAnswerProvider()
+
+ // The buffer to store the received packet. They are being reused for
+ // efficiency and it's fine because they are only ever accessed
+ // on the server thread in a sequential manner.
+ private val buffer = ByteArray(MAX_BUF_SIZE)
+ private val packet = DatagramPacket(buffer, buffer.size)
+
+ fun setAnswer(hostname: String, answer: List<InetAddress>) =
+ ansProvider.setAnswer(hostname, answer)
+
+ private fun processPacket() {
+ // Blocking read and try construct a DnsQueryPacket object.
+ socket.receive(packet)
+ val q = DnsQueryPacket(packet.data)
+ handleDnsQuery(q, packet.socketAddress)
+ }
+
+ // TODO: Add support to reply some error with a DNS reply packet with failure RCODE.
+ private fun handleDnsQuery(q: DnsQueryPacket, src: SocketAddress) {
+ val queryRecords = q.queryRecords
+ if (queryRecords.size != 1) {
+ throw IllegalArgumentException(
+ "Expected one dns query record but got ${queryRecords.size}"
+ )
+ }
+ val answerRecords = queryRecords[0].let { ansProvider.getAnswer(it.dName, it.nsType) }
+
+ if (VDBG) {
+ Log.v(TAG, "handleDnsPacket: " +
+ queryRecords.map { "${it.dName},${it.nsType}" }.joinToString() +
+ " ansCount=${answerRecords.size} socketAddress=$src")
+ }
+
+ val bytes = q.getAnswerPacket(answerRecords).bytes
+ val reply = DatagramPacket(bytes, bytes.size, src)
+ socket.send(reply)
+ }
+
+ fun start() {
+ synchronized(thread) {
+ if (status != Status.NOT_STARTED) {
+ throw IllegalStateException("unexpected status: $status")
+ }
+ thread.start()
+ status = Status.STARTED
+ }
+ }
+ fun stop() {
+ synchronized(thread) {
+ if (status != Status.STARTED) {
+ throw IllegalStateException("unexpected status: $status")
+ }
+ // The thread needs to be interrupted before closing the socket to prevent a data
+ // race where the thread tries to read from the socket while it's being closed.
+ // DatagramSocket is not thread-safe and running both concurrently can end up in
+ // getPort() returning -1 after it's been checked not to, resulting in a crash by
+ // IllegalArgumentException inside the DatagramSocket implementation.
+ thread.interrupt()
+ socket.close()
+ thread.join()
+ status = Status.STOPPED
+ }
+ }
+ val isAlive get() = thread.isAlive
+ val port get() = socket.localPort
+
+ inner class ReceivingThread : Thread() {
+ override fun run() {
+ while (!interrupted() && !socket.isClosed) {
+ try {
+ processPacket()
+ } catch (e: InterruptedException) {
+ // The caller terminated the server, exit.
+ break
+ } catch (e: SocketException) {
+ // The caller terminated the server, exit.
+ break
+ }
+ }
+ Log.i(TAG, "exiting socket={$socket}")
+ }
+ }
+
+ @VisibleForTesting(visibility = PRIVATE)
+ class DnsQueryPacket : DnsPacket {
+ constructor(data: ByteArray) : super(data)
+ constructor(header: DnsHeader, qd: List<DnsRecord>, an: List<DnsRecord>) :
+ super(header, qd, an)
+
+ init {
+ if (mHeader.isResponse) {
+ throw ParseException("Not a query packet")
+ }
+ }
+
+ val queryRecords: List<DnsRecord>
+ get() = mRecords[QDSECTION]
+
+ fun getAnswerPacket(ar: List<DnsRecord>): DnsAnswerPacket {
+ // Set QR bit of flag to 1 for response packet according to RFC 1035 section 4.1.1.
+ val flags = 1 shl 15
+ val qr = ArrayList(mRecords[QDSECTION])
+ // Copy the query packet header id to the answer packet as RFC 1035 section 4.1.1.
+ val header = DnsHeader(mHeader.id, flags, qr.size, ar.size)
+ return DnsAnswerPacket(header, qr, ar)
+ }
+ }
+
+ class DnsAnswerPacket : DnsPacket {
+ constructor(header: DnsHeader, qr: List<DnsRecord>, ar: List<DnsRecord>) :
+ super(header, qr, ar)
+ @VisibleForTesting(visibility = PRIVATE)
+ constructor(bytes: ByteArray) : super(bytes)
+ }
+}
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..740bf63
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -0,0 +1,105 @@
+/*
+ * 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
+import java.io.IOException
+
+/**
+ * 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,
+ headers: Map<String, String>? = null,
+ content: String = ""
+ ) {
+ addResponse(Request(uri.path
+ ?: "", Method.GET, uri.query ?: ""),
+ statusCode, headers, content)
+ }
+
+ /**
+ * Add a response for the given request.
+ */
+ fun addResponse(
+ request: Request,
+ statusCode: Response.IStatus,
+ headers: Map<String, String>? = null,
+ content: String = ""
+ ) {
+ val response = newFixedLengthResponse(statusCode, "text/plain", content)
+ headers?.forEach {
+ (key, value) -> response.addHeader(key, value)
+ }
+ responses[request] = response
+ }
+
+ override fun serve(session: IHTTPSession): Response {
+ val request = Request(session.uri
+ ?: "", session.method, session.queryParameterString ?: "")
+ requestsRecord.add(request)
+
+ // For PUT and POST, call parseBody to read InputStream before responding.
+ if (Method.PUT == session.method || Method.POST == session.method) {
+ try {
+ session.parseBody(HashMap())
+ } catch (e: Exception) {
+ when (e) {
+ is IOException, is ResponseException -> e.toResponse()
+ else -> throw e
+ }
+ }
+ }
+
+ // Default response is a 404
+ return responses[request] ?: super.serve(session)
+ }
+
+ fun Exception.toResponse() =
+ newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", this.toString())
+}
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..84fb47b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestNetworkTracker.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.LinkProperties
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.os.Binder
+import android.os.Build
+import androidx.annotation.RequiresApi
+import com.android.modules.utils.build.SdkLevel.isAtLeastR
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertTrue
+
+/**
+ * Create a test network based on a TUN interface with a LinkAddress.
+ *
+ * TODO: remove this function after fixing all the callers to use a list of LinkAddresses.
+ * 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 {
+ return initTestNetwork(context, listOf(interfaceAddr), setupTimeoutMs)
+}
+
+/**
+ * Create a test network based on a TUN interface with a LinkAddress list.
+ *
+ * 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,
+ linkAddrs: List<LinkAddress>,
+ setupTimeoutMs: Long = 10_000L
+): TestNetworkTracker {
+ return initTestNetwork(context, linkAddrs, lp = null, setupTimeoutMs = setupTimeoutMs)
+}
+
+/**
+ * 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].
+ *
+ * This is only usable starting from R as [TestNetworkManager] has no support for specifying
+ * LinkProperties on Q.
+ */
+@RequiresApi(Build.VERSION_CODES.R)
+fun initTestNetwork(
+ context: Context,
+ lp: LinkProperties,
+ setupTimeoutMs: Long = 10_000L
+): TestNetworkTracker {
+ return initTestNetwork(context, lp.linkAddresses, lp, setupTimeoutMs)
+}
+
+private fun initTestNetwork(
+ context: Context,
+ linkAddrs: List<LinkAddress>,
+ lp: LinkProperties?,
+ setupTimeoutMs: Long = 10_000L
+): TestNetworkTracker {
+ val tnm = context.getSystemService(TestNetworkManager::class.java)!!
+ val iface = if (isAtLeastS()) tnm.createTunInterface(linkAddrs)
+ else tnm.createTunInterface(linkAddrs.toTypedArray())
+ val lpWithIface = if (lp == null) null else LinkProperties(lp).apply {
+ interfaceName = iface.interfaceName
+ }
+ return TestNetworkTracker(context, iface, tnm, lpWithIface, 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,
+ val tnm: TestNetworkManager,
+ val lp: LinkProperties?,
+ setupTimeoutMs: Long
+) : TestableNetworkCallback.HasNetwork {
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val binder = Binder()
+
+ private val networkCallback: NetworkCallback
+ override val network: Network
+ val testIface: TestNetworkInterface
+
+ 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)
+
+ network = try {
+ if (lp != null) {
+ assertTrue(isAtLeastR(), "Cannot specify TestNetwork LinkProperties before R")
+ tnm.setupTestNetwork(lp, true /* isMetered */, binder)
+ } else {
+ tnm.setupTestNetwork(iface.interfaceName, binder)
+ }
+ networkFuture.get(setupTimeoutMs, TimeUnit.MILLISECONDS)
+ } catch (e: Throwable) {
+ cm.unregisterNetworkCallback(networkCallback)
+ throw e
+ }
+
+ testIface = iface
+ }
+
+ fun teardown() {
+ cm.unregisterNetworkCallback(networkCallback)
+ tnm.teardownTestNetwork(network)
+ }
+}
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..f571f64
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestPermissionUtil.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.modules.utils.build.SdkLevel
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import com.android.testutils.FunctionalUtils.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
+
+ // Calls to adoptShellPermissionIdentity do not nest, and dropShellPermissionIdentity drops all
+ // permissions. Thus, nesting calls will almost certainly cause test bugs, On S+, where we can
+ // detect this, refuse to do it.
+ //
+ // TODO: when R is deprecated, we could try to make this work instead.
+ // - Get the list of previously-adopted permissions.
+ // - Adopt the union of the previously-adopted and newly-requested permissions.
+ // - Run the task.
+ // - Adopt the previously-adopted permissions, dropping the ones just adopted.
+ //
+ // This would allow tests (and utility classes, such as the TestCarrierConfigReceiver attempted
+ // in aosp/2106007) to call runAsShell even within a test that has already adopted permissions.
+ if (SdkLevel.isAtLeastS() && !autom.getAdoptedShellPermissions().isNullOrEmpty()) {
+ throw IllegalStateException("adoptShellPermissionIdentity calls must not be nested")
+ }
+
+ 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/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
new file mode 100644
index 0000000..8dc1bc4
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -0,0 +1,206 @@
+/*
+ * 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.content.Context
+import android.net.KeepalivePacketData
+import android.net.LinkProperties
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.QosFilter
+import android.net.Uri
+import android.os.Looper
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkDestroyed
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkUnwanted
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnRegisterQosCallback
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnRemoveKeepalivePacketFilter
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnSaveAcceptUnvalidated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnSignalStrengthThresholdsUpdated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStartSocketKeepalive
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.time.Duration
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.junit.Assert.assertArrayEquals
+
+// Any legal score (0~99) for the test network would do, as it is going to be kept up by the
+// requests filed by the test and should never match normal internet requests. 70 is the default
+// score of Ethernet networks, it's as good a value as any other.
+private const val TEST_NETWORK_SCORE = 70
+
+private class Provider(context: Context, looper: Looper) :
+ NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider")
+
+public open class TestableNetworkAgent(
+ context: Context,
+ looper: Looper,
+ val nc: NetworkCapabilities,
+ val lp: LinkProperties,
+ conf: NetworkAgentConfig
+) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
+ nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+
+ val DEFAULT_TIMEOUT_MS = 5000L
+
+ val history = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+ sealed class CallbackEntry {
+ object OnBandwidthUpdateRequested : CallbackEntry()
+ object OnNetworkUnwanted : CallbackEntry()
+ data class OnAddKeepalivePacketFilter(
+ val slot: Int,
+ val packet: KeepalivePacketData
+ ) : CallbackEntry()
+ data class OnRemoveKeepalivePacketFilter(val slot: Int) : CallbackEntry()
+ data class OnStartSocketKeepalive(
+ val slot: Int,
+ val interval: Int,
+ val packet: KeepalivePacketData
+ ) : CallbackEntry()
+ data class OnStopSocketKeepalive(val slot: Int) : CallbackEntry()
+ data class OnSaveAcceptUnvalidated(val accept: Boolean) : CallbackEntry()
+ object OnAutomaticReconnectDisabled : CallbackEntry()
+ data class OnValidationStatus(val status: Int, val uri: Uri?) : CallbackEntry()
+ data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry()
+ object OnNetworkCreated : CallbackEntry()
+ object OnNetworkDestroyed : CallbackEntry()
+ data class OnDscpPolicyStatusUpdated(val policyId: Int, val status: Int) : CallbackEntry()
+ data class OnRegisterQosCallback(
+ val callbackId: Int,
+ val filter: QosFilter
+ ) : CallbackEntry()
+ data class OnUnregisterQosCallback(val callbackId: Int) : CallbackEntry()
+ }
+
+ override fun onBandwidthUpdateRequested() {
+ history.add(OnBandwidthUpdateRequested)
+ }
+
+ override fun onNetworkUnwanted() {
+ history.add(OnNetworkUnwanted)
+ }
+
+ override fun onAddKeepalivePacketFilter(slot: Int, packet: KeepalivePacketData) {
+ history.add(OnAddKeepalivePacketFilter(slot, packet))
+ }
+
+ override fun onRemoveKeepalivePacketFilter(slot: Int) {
+ history.add(OnRemoveKeepalivePacketFilter(slot))
+ }
+
+ override fun onStartSocketKeepalive(
+ slot: Int,
+ interval: Duration,
+ packet: KeepalivePacketData
+ ) {
+ history.add(OnStartSocketKeepalive(slot, interval.seconds.toInt(), packet))
+ }
+
+ override fun onStopSocketKeepalive(slot: Int) {
+ history.add(OnStopSocketKeepalive(slot))
+ }
+
+ override fun onSaveAcceptUnvalidated(accept: Boolean) {
+ history.add(OnSaveAcceptUnvalidated(accept))
+ }
+
+ override fun onAutomaticReconnectDisabled() {
+ history.add(OnAutomaticReconnectDisabled)
+ }
+
+ override fun onSignalStrengthThresholdsUpdated(thresholds: IntArray) {
+ history.add(OnSignalStrengthThresholdsUpdated(thresholds))
+ }
+
+ fun expectSignalStrengths(thresholds: IntArray? = intArrayOf()) {
+ expectCallback<OnSignalStrengthThresholdsUpdated>().let {
+ assertArrayEquals(thresholds, it.thresholds)
+ }
+ }
+
+ override fun onQosCallbackRegistered(qosCallbackId: Int, filter: QosFilter) {
+ history.add(OnRegisterQosCallback(qosCallbackId, filter))
+ }
+
+ override fun onQosCallbackUnregistered(qosCallbackId: Int) {
+ history.add(OnUnregisterQosCallback(qosCallbackId))
+ }
+
+ override fun onValidationStatus(status: Int, uri: Uri?) {
+ history.add(OnValidationStatus(status, uri))
+ }
+
+ override fun onNetworkCreated() {
+ history.add(OnNetworkCreated)
+ }
+
+ override fun onNetworkDestroyed() {
+ history.add(OnNetworkDestroyed)
+ }
+
+ override fun onDscpPolicyStatusUpdated(policyId: Int, status: Int) {
+ history.add(OnDscpPolicyStatusUpdated(policyId, status))
+ }
+
+ // Expects the initial validation event that always occurs immediately after registering
+ // a NetworkAgent whose network does not require validation (which test networks do
+ // not, since they lack the INTERNET capability). It always contains the default argument
+ // for the URI.
+ fun expectValidationBypassedStatus() = expectCallback<OnValidationStatus>().let {
+ assertEquals(it.status, VALID_NETWORK)
+ // The returned Uri is parsed from the empty string, which means it's an
+ // instance of the (private) Uri.StringUri. There are no real good ways
+ // to check this, the least bad is to just convert it to a string and
+ // make sure it's empty.
+ assertEquals("", it.uri.toString())
+ }
+
+ inline fun <reified T : CallbackEntry> expectCallback(): T {
+ val foundCallback = history.poll(DEFAULT_TIMEOUT_MS)
+ assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback")
+ return foundCallback
+ }
+
+ inline fun <reified T : CallbackEntry> expectCallback(valid: (T) -> Boolean) {
+ val foundCallback = history.poll(DEFAULT_TIMEOUT_MS)
+ assertTrue(foundCallback is T, "Expected ${T::class} but found $foundCallback")
+ assertTrue(valid(foundCallback), "Unexpected callback : $foundCallback")
+ }
+
+ inline fun <reified T : CallbackEntry> eventuallyExpect() =
+ history.poll(DEFAULT_TIMEOUT_MS) { it is T }.also {
+ assertNotNull(it, "Callback ${T::class} not received")
+ } as T
+
+ fun assertNoCallback() {
+ assertTrue(waitForIdle(DEFAULT_TIMEOUT_MS),
+ "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms")
+ assertNull(history.peek())
+ }
+}
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..df9c61a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -0,0 +1,569 @@
+/*
+ * 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.fail
+
+object NULL_NETWORK : Network(-1)
+object ANY_NETWORK : Network(-2)
+fun anyNetwork() = ANY_NETWORK
+
+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 reason: 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) {
+ Log.d(TAG, "onAvailable $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 = 30_000L // ms
+private const val DEFAULT_NO_CALLBACK_TIMEOUT = 200L // ms
+private val NOOP = Runnable {}
+
+/**
+ * See comments on the public constructor below for a description of the arguments.
+ */
+open class TestableNetworkCallback private constructor(
+ src: TestableNetworkCallback?,
+ val defaultTimeoutMs: Long = DEFAULT_TIMEOUT,
+ val defaultNoCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
+ val waiterFunc: Runnable = NOOP // "() -> Unit" would forbid calling with a void func from Java
+) : RecorderCallback(src) {
+ /**
+ * Construct a testable network callback.
+ * @param timeoutMs the default timeout for expecting a callback. Default 30 seconds. This
+ * should be long in most cases, because the success case doesn't incur
+ * the wait.
+ * @param noCallbackTimeoutMs the timeout for expecting that no callback is received. Default
+ * 200ms. Because the success case does incur the timeout, this
+ * should be short in most cases, but not so short as to frequently
+ * time out before an incorrect callback is received.
+ * @param waiterFunc a function to use before asserting no callback. For some specific tests,
+ * it is useful to run test-specific code before asserting no callback to
+ * increase the likelihood that a spurious callback is correctly detected.
+ * As an example, a unit test using mock loopers may want to use this to
+ * make sure the loopers are drained before asserting no callback, since
+ * one of them may cause a callback to be called. @see ConnectivityServiceTest
+ * for such an example.
+ */
+ @JvmOverloads
+ constructor(
+ timeoutMs: Long = DEFAULT_TIMEOUT,
+ noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
+ waiterFunc: Runnable = NOOP
+ ) : this(null, timeoutMs, noCallbackTimeoutMs, waiterFunc)
+
+ fun createLinkedCopy() = TestableNetworkCallback(
+ this, defaultTimeoutMs, defaultNoCallbackTimeoutMs, waiterFunc)
+
+ // 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
+ }
+
+ /**
+ * Get the next callback or null if timeout.
+ *
+ * With no argument, this method waits out the default timeout. To wait forever, pass
+ * Long.MAX_VALUE.
+ */
+ @JvmOverloads
+ fun poll(timeoutMs: Long = defaultTimeoutMs, predicate: (CallbackEntry) -> Boolean = { true }) =
+ history.poll(timeoutMs, predicate)
+
+ /*****
+ * expect family of methods.
+ * These methods fetch the next callback and assert it matches the conditions : type,
+ * passed predicate. If no callback is received within the timeout, these methods fail.
+ */
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ network: Network = ANY_NETWORK,
+ timeoutMs: Long = defaultTimeoutMs,
+ errorMsg: String? = null,
+ test: (T) -> Boolean = { true }
+ ) = expect<CallbackEntry>(network, timeoutMs, errorMsg) {
+ if (type.isInstance(it)) {
+ test(it as T) // Cast can't fail since type.isInstance(it) and type: KClass<T>
+ } else {
+ fail("Expected callback ${type.simpleName}, got $it")
+ }
+ } as T
+
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ network: HasNetwork,
+ timeoutMs: Long = defaultTimeoutMs,
+ errorMsg: String? = null,
+ test: (T) -> Boolean = { true }
+ ) = expect(type, network.network, timeoutMs, errorMsg, test)
+
+ // Java needs an explicit overload to let it omit arguments in the middle, so define these
+ // here. Note that @JvmOverloads give us the versions without the last arguments too, so
+ // there is no need to explicitly define versions without the test predicate.
+ // Without |network|
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ timeoutMs: Long,
+ errorMsg: String?,
+ test: (T) -> Boolean = { true }
+ ) = expect(type, ANY_NETWORK, timeoutMs, errorMsg, test)
+
+ // Without |timeout|, in Network and HasNetwork versions
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ network: Network,
+ errorMsg: String?,
+ test: (T) -> Boolean = { true }
+ ) = expect(type, network, defaultTimeoutMs, errorMsg, test)
+
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ network: HasNetwork,
+ errorMsg: String?,
+ test: (T) -> Boolean = { true }
+ ) = expect(type, network.network, defaultTimeoutMs, errorMsg, test)
+
+ // Without |errorMsg|, in Network and HasNetwork versions
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ network: Network,
+ timeoutMs: Long,
+ test: (T) -> Boolean
+ ) = expect(type, network, timeoutMs, null, test)
+
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ network: HasNetwork,
+ timeoutMs: Long,
+ test: (T) -> Boolean
+ ) = expect(type, network.network, timeoutMs, null, test)
+
+ // Without |network| or |timeout|
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ errorMsg: String?,
+ test: (T) -> Boolean = { true }
+ ) = expect(type, ANY_NETWORK, defaultTimeoutMs, errorMsg, test)
+
+ // Without |network| or |errorMsg|
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ timeoutMs: Long,
+ test: (T) -> Boolean = { true }
+ ) = expect(type, ANY_NETWORK, timeoutMs, null, test)
+
+ // Without |timeout| or |errorMsg|, in Network and HasNetwork versions
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ network: Network,
+ test: (T) -> Boolean
+ ) = expect(type, network, defaultTimeoutMs, null, test)
+
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ network: HasNetwork,
+ test: (T) -> Boolean
+ ) = expect(type, network.network, defaultTimeoutMs, null, test)
+
+ // Without |network| or |timeout| or |errorMsg|
+ @JvmOverloads
+ fun <T : CallbackEntry> expect(
+ type: KClass<T>,
+ test: (T) -> Boolean
+ ) = expect(type, ANY_NETWORK, defaultTimeoutMs, null, test)
+
+ // Kotlin reified versions. Don't call methods above, or the predicate would need to be noinline
+ inline fun <reified T : CallbackEntry> expect(
+ network: Network = ANY_NETWORK,
+ timeoutMs: Long = defaultTimeoutMs,
+ errorMsg: String? = null,
+ test: (T) -> Boolean = { true }
+ ) = (poll(timeoutMs) ?: fail("Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
+ .also {
+ if (it !is T) fail("Expected callback ${T::class.simpleName}, got $it")
+ if (ANY_NETWORK !== network && it.network != network) {
+ fail("Expected network $network for callback : $it")
+ }
+ if (!test(it)) {
+ fail("${errorMsg ?: "Callback doesn't match predicate"} : $it")
+ }
+ } as T
+
+ inline fun <reified T : CallbackEntry> expect(
+ network: HasNetwork,
+ timeoutMs: Long = defaultTimeoutMs,
+ errorMsg: String? = null,
+ test: (T) -> Boolean = { true }
+ ) = expect(network.network, timeoutMs, errorMsg, test)
+
+ /*****
+ * assertNoCallback family of methods.
+ * These methods make sure that no callback that matches the predicate was received.
+ * If no predicate is given, they make sure that no callback at all was received.
+ * These methods run the waiter func given in the constructor if any.
+ */
+ @JvmOverloads
+ fun assertNoCallback(
+ timeoutMs: Long = defaultNoCallbackTimeoutMs,
+ valid: (CallbackEntry) -> Boolean = { true }
+ ) {
+ waiterFunc.run()
+ history.poll(timeoutMs) { valid(it) }?.let { fail("Expected no callback but got $it") }
+ }
+
+ fun assertNoCallback(valid: (CallbackEntry) -> Boolean) =
+ assertNoCallback(defaultNoCallbackTimeoutMs, valid)
+
+ /*****
+ * eventuallyExpect family of methods.
+ * These methods make sure a callback that matches the type/predicate is received eventually.
+ * Any callback of the wrong type, or doesn't match the optional predicate, is ignored.
+ * They fail if no callback matching the predicate is received within the timeout.
+ */
+ inline fun <reified T : CallbackEntry> eventuallyExpect(
+ timeoutMs: Long = defaultTimeoutMs,
+ from: Int = mark,
+ crossinline predicate: (T) -> Boolean = { true }
+ ): T = history.poll(timeoutMs, from) { it is T && predicate(it) }.also {
+ assertNotNull(it, "Callback ${T::class} not received within ${timeoutMs}ms. " +
+ "Got ${history.backtrace()}")
+ } as T
+
+ @JvmOverloads
+ fun <T : CallbackEntry> eventuallyExpect(
+ type: KClass<T>,
+ timeoutMs: Long = defaultTimeoutMs,
+ predicate: (cb: T) -> Boolean = { true }
+ ) = history.poll(timeoutMs) { type.java.isInstance(it) && predicate(it as T) }.also {
+ assertNotNull(it, "Callback ${type.java} not received within ${timeoutMs}ms. " +
+ "Got ${history.backtrace()}")
+ } as T
+
+ fun <T : CallbackEntry> eventuallyExpect(
+ type: KClass<T>,
+ timeoutMs: Long = defaultTimeoutMs,
+ from: Int = mark,
+ predicate: (cb: T) -> Boolean = { true }
+ ) = history.poll(timeoutMs, from) { type.java.isInstance(it) && predicate(it as T) }.also {
+ assertNotNull(it, "Callback ${type.java} not received within ${timeoutMs}ms. " +
+ "Got ${history.backtrace()}")
+ } as T
+
+ // 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.
+ @JvmOverloads
+ fun expectAvailableCallbacks(
+ net: Network,
+ suspended: Boolean = false,
+ validated: Boolean? = true,
+ blocked: Boolean = false,
+ tmt: Long = defaultTimeoutMs
+ ) {
+ expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+ expect<BlockedStatus>(net, tmt) { it.blocked == blocked }
+ }
+
+ fun expectAvailableCallbacks(
+ net: Network,
+ suspended: Boolean,
+ validated: Boolean,
+ blockedReason: Int,
+ tmt: Long
+ ) {
+ expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+ expect<BlockedStatusInt>(net) { it.reason == blockedReason }
+ }
+
+ private fun expectAvailableCallbacksCommon(
+ net: Network,
+ suspended: Boolean,
+ validated: Boolean?,
+ tmt: Long
+ ) {
+ expect<Available>(net, tmt)
+ if (suspended) {
+ expect<Suspended>(net, tmt)
+ }
+ expect<CapabilitiesChanged>(net, tmt) {
+ validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ expect<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)
+
+ // 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, expect<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)
+ expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+ }
+
+ fun expectAvailableThenValidatedCallbacks(
+ net: Network,
+ blockedReason: Int,
+ tmt: Long = defaultTimeoutMs
+ ) {
+ expectAvailableCallbacks(net, validated = false, suspended = false,
+ blockedReason = blockedReason, tmt = tmt)
+ expectCaps(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
+ }
+
+ 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 expectCaps(
+ n: HasNetwork,
+ tmt: Long = defaultTimeoutMs,
+ valid: (NetworkCapabilities) -> Boolean = { true }
+ ) = expect<CapabilitiesChanged>(n.network, tmt) { valid(it.caps) }.caps
+
+ @JvmOverloads
+ fun expectCaps(
+ n: Network,
+ tmt: Long = defaultTimeoutMs,
+ valid: (NetworkCapabilities) -> Boolean
+ ) = expect<CapabilitiesChanged>(n, tmt) { valid(it.caps) }.caps
+
+ fun expectCaps(
+ n: HasNetwork,
+ valid: (NetworkCapabilities) -> Boolean
+ ) = expect<CapabilitiesChanged>(n.network) { valid(it.caps) }.caps
+
+ fun expectCaps(
+ tmt: Long,
+ valid: (NetworkCapabilities) -> Boolean
+ ) = expect<CapabilitiesChanged>(ANY_NETWORK, tmt) { valid(it.caps) }.caps
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
new file mode 100644
index 0000000..21bd60c
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import kotlin.test.fail
+
+class TestableNetworkOfferCallback(val timeoutMs: Long, private val noCallbackTimeoutMs: Long)
+ : NetworkProvider.NetworkOfferCallback {
+ private val TAG = this::class.simpleName
+ val history = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+ sealed class CallbackEntry {
+ data class OnNetworkNeeded(val request: NetworkRequest) : CallbackEntry()
+ data class OnNetworkUnneeded(val request: NetworkRequest) : CallbackEntry()
+ }
+
+ /**
+ * Called by the system when a network for this offer is needed to satisfy some
+ * networking request.
+ */
+ override fun onNetworkNeeded(request: NetworkRequest) {
+ Log.d(TAG, "onNetworkNeeded $request")
+ history.add(CallbackEntry.OnNetworkNeeded(request))
+ }
+
+ /**
+ * Called by the system when this offer is no longer valuable for this request.
+ */
+ override fun onNetworkUnneeded(request: NetworkRequest) {
+ Log.d(TAG, "onNetworkUnneeded $request")
+ history.add(CallbackEntry.OnNetworkUnneeded(request))
+ }
+
+ inline fun <reified T : CallbackEntry> expectCallbackThat(
+ crossinline predicate: (T) -> Boolean
+ ) {
+ val event = history.poll(timeoutMs)
+ ?: fail("Did not receive callback after ${timeoutMs}ms")
+ if (event !is T || !predicate(event)) fail("Received unexpected callback $event")
+ }
+
+ fun expectOnNetworkNeeded(capabilities: NetworkCapabilities) =
+ expectCallbackThat<CallbackEntry.OnNetworkNeeded> {
+ it.request.canBeSatisfiedBy(capabilities)
+ }
+
+ fun expectOnNetworkUnneeded(capabilities: NetworkCapabilities) =
+ expectCallbackThat<CallbackEntry.OnNetworkUnneeded> {
+ it.request.canBeSatisfiedBy(capabilities)
+ }
+
+ fun assertNoCallback() {
+ val cb = history.poll(noCallbackTimeoutMs)
+ if (null != cb) fail("Expected no callback but got $cb")
+ }
+}
\ No newline at end of file
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..4a7b351
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProvider.kt
@@ -0,0 +1,123 @@
+/*
+ * 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 android.util.Log
+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
+const val TOKEN_ANY = -1
+
+open class TestableNetworkStatsProvider(
+ val defaultTimeoutMs: Long = DEFAULT_TIMEOUT_MS
+) : NetworkStatsProvider() {
+ sealed class CallbackType {
+ data class OnRequestStatsUpdate(val token: Int) : CallbackType()
+ data class OnSetWarningAndLimit(
+ val iface: String,
+ val warningBytes: Long,
+ val limitBytes: Long
+ ) : CallbackType()
+ data class OnSetLimit(val iface: String, val limitBytes: Long) : CallbackType() {
+ // Add getter for backward compatibility since old tests do not recognize limitBytes.
+ val quotaBytes: Long
+ get() = limitBytes
+ }
+ data class OnSetAlert(val quotaBytes: Long) : CallbackType()
+ }
+
+ private val TAG = this::class.simpleName
+ val history = ArrayTrackRecord<CallbackType>().newReadHead()
+ // See ReadHead#mark
+ val mark get() = history.mark
+
+ override fun onRequestStatsUpdate(token: Int) {
+ Log.d(TAG, "onRequestStatsUpdate $token")
+ history.add(CallbackType.OnRequestStatsUpdate(token))
+ }
+
+ override fun onSetWarningAndLimit(iface: String, warningBytes: Long, limitBytes: Long) {
+ Log.d(TAG, "onSetWarningAndLimit $iface $warningBytes $limitBytes")
+ history.add(CallbackType.OnSetWarningAndLimit(iface, warningBytes, limitBytes))
+ }
+
+ override fun onSetLimit(iface: String, quotaBytes: Long) {
+ Log.d(TAG, "onSetLimit $iface $quotaBytes")
+ history.add(CallbackType.OnSetLimit(iface, quotaBytes))
+ }
+
+ override fun onSetAlert(quotaBytes: Long) {
+ Log.d(TAG, "onSetAlert $quotaBytes")
+ history.add(CallbackType.OnSetAlert(quotaBytes))
+ }
+
+ fun expectOnRequestStatsUpdate(token: Int, timeout: Long = defaultTimeoutMs): Int {
+ val event = history.poll(timeout)
+ assertTrue(event is CallbackType.OnRequestStatsUpdate)
+ if (token != TOKEN_ANY) {
+ assertEquals(token, event.token)
+ }
+ return event.token
+ }
+
+ 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..5547c90
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkStatsProviderCbBinder.kt
@@ -0,0 +1,100 @@
+/*
+ * 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 NotifyWarningReached : CallbackType()
+ object NotifyLimitReached : 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 notifyWarningReached() {
+ history.add(CallbackType.NotifyWarningReached)
+ }
+
+ override fun notifyLimitReached() {
+ history.add(CallbackType.NotifyLimitReached)
+ }
+
+ override fun notifyWarningOrLimitReached() {
+ // Older callback is split into notifyLimitReached and notifyWarningReached in T.
+ 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 expectNotifyWarningReached() =
+ assertEquals(CallbackType.NotifyWarningReached, history.poll(DEFAULT_TIMEOUT_MS))
+
+ fun expectNotifyLimitReached() =
+ assertEquals(CallbackType.NotifyLimitReached, history.poll(DEFAULT_TIMEOUT_MS))
+
+ 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/devicetests/com/android/testutils/async/FakeOsAccess.java b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
new file mode 100644
index 0000000..48b57d7
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2023 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.async;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+import android.util.Log;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+import com.android.net.module.util.async.OsAccess;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+public class FakeOsAccess extends OsAccess {
+ public static final boolean ENABLE_FINE_DEBUG = true;
+
+ public static final int DEFAULT_FILE_DATA_QUEUE_SIZE = 8 * 1024;
+
+ private enum FileType { PAIR, PIPE }
+
+ // Common poll() constants:
+ private static final short POLLIN = 0x0001;
+ private static final short POLLOUT = 0x0004;
+ private static final short POLLERR = 0x0008;
+ private static final short POLLHUP = 0x0010;
+
+ private static final Constructor<FileDescriptor> FD_CONSTRUCTOR;
+ private static final Field FD_FIELD_DESCRIPTOR;
+ private static final Field PFD_FIELD_DESCRIPTOR;
+ private static final Field PFD_FIELD_GUARD;
+ private static final Method CLOSE_GUARD_METHOD_CLOSE;
+
+ private final int mReadQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+ private final int mWriteQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+ private final HashMap<Integer, File> mFiles = new HashMap<>();
+ private final byte[] mTmpBuffer = new byte[1024];
+ private final long mStartTime;
+ private final String mLogTag;
+ private int mFileNumberGen = 3;
+ private boolean mHasRateLimitedData;
+
+ public FakeOsAccess(String logTag) {
+ mLogTag = logTag;
+ mStartTime = monotonicTimeMillis();
+ }
+
+ @Override
+ public long monotonicTimeMillis() {
+ return System.nanoTime() / 1000000;
+ }
+
+ @Override
+ public FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd) {
+ try {
+ return (FileDescriptor) PFD_FIELD_DESCRIPTOR.get(fd);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close(ParcelFileDescriptor fd) {
+ if (fd != null) {
+ close(getInnerFileDescriptor(fd));
+
+ try {
+ // Reduce CloseGuard warnings.
+ Object guard = PFD_FIELD_GUARD.get(fd);
+ CLOSE_GUARD_METHOD_CLOSE.invoke(guard);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public synchronized void close(FileDescriptor fd) {
+ if (fd != null) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.decreaseRefCount();
+ mFiles.remove(getFileDescriptorNumber(fd));
+ setFileDescriptorNumber(fd, -1);
+ notifyAll();
+ }
+ }
+ }
+
+ private File getFile(String func, FileDescriptor fd) throws IOException {
+ File file = getFileOrNull(fd);
+ if (file == null) {
+ throw newIOException(func, "Unknown file descriptor: " + getFileDebugName(fd));
+ }
+ return file;
+ }
+
+ private File getFileOrNull(FileDescriptor fd) {
+ return mFiles.get(getFileDescriptorNumber(fd));
+ }
+
+ @Override
+ public String getFileDebugName(ParcelFileDescriptor fd) {
+ return (fd != null ? getFileDebugName(getInnerFileDescriptor(fd)) : "null");
+ }
+
+ public String getFileDebugName(FileDescriptor fd) {
+ if (fd == null) {
+ return "null";
+ }
+
+ final int fdNumber = getFileDescriptorNumber(fd);
+ File file = mFiles.get(fdNumber);
+
+ StringBuilder sb = new StringBuilder();
+ if (file != null) {
+ if (file.name != null) {
+ sb.append(file.name);
+ sb.append("/");
+ }
+ sb.append(file.type);
+ sb.append("/");
+ } else {
+ sb.append("BADFD/");
+ }
+ sb.append(fdNumber);
+ return sb.toString();
+ }
+
+ public synchronized void setFileName(FileDescriptor fd, String name) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.name = name;
+ }
+ }
+
+ @Override
+ public synchronized void setNonBlocking(FileDescriptor fd) throws IOException {
+ File file = getFile("fcntl", fd);
+ file.isBlocking = false;
+ }
+
+ @Override
+ public synchronized int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException {
+ checkBoundaries("read", buffer, pos, len);
+
+ File file = getFile("read", fd);
+ if (file.readQueue == null) {
+ throw newIOException("read", "File not readable");
+ }
+ file.checkNonBlocking("read");
+
+ if (len == 0) {
+ return 0;
+ }
+
+ final int availSize = file.readQueue.size();
+ if (availSize == 0) {
+ if (file.isEndOfStream) {
+ // Java convention uses -1 to indicate end of stream.
+ return -1;
+ }
+ return 0; // EAGAIN
+ }
+
+ final int readCount = Math.min(len, availSize);
+ file.readQueue.readBytes(buffer, pos, readCount);
+ maybeTransferData(file);
+ return readCount;
+ }
+
+ @Override
+ public synchronized int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException {
+ checkBoundaries("write", buffer, pos, len);
+
+ File file = getFile("write", fd);
+ if (file.writeQueue == null) {
+ throw newIOException("read", "File not writable");
+ }
+ if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+ throw newIOException("write", "The other end of pipe is closed");
+ }
+ file.checkNonBlocking("write");
+
+ if (len == 0) {
+ return 0;
+ }
+
+ final int originalFreeSize = file.writeQueue.freeSize();
+ if (originalFreeSize == 0) {
+ return 0; // EAGAIN
+ }
+
+ final int writeCount = Math.min(len, originalFreeSize);
+ file.writeQueue.writeBytes(buffer, pos, writeCount);
+ maybeTransferData(file);
+
+ if (file.writeQueue.freeSize() < originalFreeSize) {
+ final int additionalQueuedCount = originalFreeSize - file.writeQueue.freeSize();
+ Log.i(mLogTag, logStr("Delaying transfer of " + additionalQueuedCount
+ + " bytes, queued=" + file.writeQueue.size() + ", type=" + file.type
+ + ", src_red=" + file.outboundLimiter + ", dst_red=" + file.sink.inboundLimiter));
+ }
+
+ return writeCount;
+ }
+
+ private void maybeTransferData(File file) {
+ boolean hasChanges = copyFileBuffers(file, file.sink);
+ hasChanges = copyFileBuffers(file.source, file) || hasChanges;
+
+ if (hasChanges) {
+ // TODO(b/245971639): Avoid notifying if no-one is polling.
+ notifyAll();
+ }
+ }
+
+ private boolean copyFileBuffers(File src, File dst) {
+ if (src.writeQueue == null || dst.readQueue == null) {
+ return false;
+ }
+
+ final int originalCopyCount = Math.min(mTmpBuffer.length,
+ Math.min(src.writeQueue.size(), dst.readQueue.freeSize()));
+
+ final int allowedCopyCount = RateLimiter.limit(
+ src.outboundLimiter, dst.inboundLimiter, originalCopyCount);
+
+ if (allowedCopyCount < originalCopyCount) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.i(mLogTag, logStr("Delaying transfer of "
+ + (originalCopyCount - allowedCopyCount) + " bytes, original="
+ + originalCopyCount + ", allowed=" + allowedCopyCount
+ + ", type=" + src.type));
+ }
+ if (originalCopyCount > 0) {
+ mHasRateLimitedData = true;
+ }
+ if (allowedCopyCount == 0) {
+ return false;
+ }
+ }
+
+ boolean hasChanges = false;
+ if (allowedCopyCount > 0) {
+ if (dst.readQueue.size() == 0 || src.writeQueue.freeSize() == 0) {
+ hasChanges = true; // Read queue had no data, or write queue was full.
+ }
+ src.writeQueue.readBytes(mTmpBuffer, 0, allowedCopyCount);
+ dst.readQueue.writeBytes(mTmpBuffer, 0, allowedCopyCount);
+ }
+
+ if (!dst.isEndOfStream && src.openCount == 0
+ && src.writeQueue.size() == 0 && dst.readQueue.size() == 0) {
+ dst.isEndOfStream = true;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+
+ public void clearInboundRateLimit(FileDescriptor fd) {
+ setInboundRateLimit(fd, Integer.MAX_VALUE);
+ }
+
+ public void clearOutboundRateLimit(FileDescriptor fd) {
+ setOutboundRateLimit(fd, Integer.MAX_VALUE);
+ }
+
+ public synchronized void setInboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.inboundLimiter.setBytesPerSecond(bytesPerSecond);
+ maybeTransferData(file);
+ }
+ }
+
+ public synchronized void setOutboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.outboundLimiter.setBytesPerSecond(bytesPerSecond);
+ maybeTransferData(file);
+ }
+ }
+
+ public synchronized ParcelFileDescriptor[] socketpair() throws IOException {
+ int fdNumber1 = getNextFd("socketpair");
+ int fdNumber2 = getNextFd("socketpair");
+
+ File file1 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+ File file2 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+
+ return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+ }
+
+ @Override
+ public synchronized ParcelFileDescriptor[] pipe() throws IOException {
+ int fdNumber1 = getNextFd("pipe");
+ int fdNumber2 = getNextFd("pipe");
+
+ File file1 = new File(FileType.PIPE, mReadQueueSize, 0);
+ File file2 = new File(FileType.PIPE, 0, mWriteQueueSize);
+
+ return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+ }
+
+ private ParcelFileDescriptor[] registerFilePair(
+ int fdNumber1, File file1, int fdNumber2, File file2) {
+ file1.sink = file2;
+ file1.source = file2;
+ file2.sink = file1;
+ file2.source = file1;
+
+ mFiles.put(fdNumber1, file1);
+ mFiles.put(fdNumber2, file2);
+ return new ParcelFileDescriptor[] {
+ newParcelFileDescriptor(fdNumber1), newParcelFileDescriptor(fdNumber2)};
+ }
+
+ @Override
+ public short getPollInMask() {
+ return POLLIN;
+ }
+
+ @Override
+ public short getPollOutMask() {
+ return POLLOUT;
+ }
+
+ @Override
+ public synchronized int poll(StructPollfd[] fds, int timeoutMs) throws IOException {
+ if (timeoutMs < 0) {
+ timeoutMs = (int) TimeUnit.HOURS.toMillis(1); // Make "infinite" equal to 1 hour.
+ }
+
+ if (fds == null || fds.length > 1000) {
+ throw newIOException("poll", "Invalid fds param");
+ }
+ for (StructPollfd pollFd : fds) {
+ getFile("poll", pollFd.fd);
+ }
+
+ int waitCallCount = 0;
+ final long deadline = monotonicTimeMillis() + timeoutMs;
+ while (true) {
+ if (mHasRateLimitedData) {
+ mHasRateLimitedData = false;
+ for (File file : mFiles.values()) {
+ if (file.inboundLimiter.getLastRequestReduction() != 0) {
+ copyFileBuffers(file.source, file);
+ }
+ if (file.outboundLimiter.getLastRequestReduction() != 0) {
+ copyFileBuffers(file, file.sink);
+ }
+ }
+ }
+
+ final int readyCount = calculateReadyCount(fds);
+ if (readyCount > 0) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("Poll returns " + readyCount
+ + " after " + waitCallCount + " wait calls"));
+ }
+ return readyCount;
+ }
+
+ long remainingTimeoutMs = deadline - monotonicTimeMillis();
+ if (remainingTimeoutMs <= 0) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("Poll timeout " + timeoutMs
+ + "ms after " + waitCallCount + " wait calls"));
+ }
+ return 0;
+ }
+
+ if (mHasRateLimitedData) {
+ remainingTimeoutMs = Math.min(RateLimiter.BUCKET_DURATION_MS, remainingTimeoutMs);
+ }
+
+ try {
+ wait(remainingTimeoutMs);
+ } catch (InterruptedException e) {
+ // Ignore and retry
+ }
+ waitCallCount++;
+ }
+ }
+
+ private int calculateReadyCount(StructPollfd[] fds) {
+ int fdCount = 0;
+ for (StructPollfd pollFd : fds) {
+ pollFd.revents = 0;
+
+ File file = getFileOrNull(pollFd.fd);
+ if (file == null) {
+ Log.w(mLogTag, logStr("Ignoring FD concurrently closed by a buggy app: "
+ + getFileDebugName(pollFd.fd)));
+ continue;
+ }
+
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("calculateReadyCount fd=" + getFileDebugName(pollFd.fd)
+ + ", events=" + pollFd.events + ", eof=" + file.isEndOfStream
+ + ", r=" + (file.readQueue != null ? file.readQueue.size() : -1)
+ + ", w=" + (file.writeQueue != null ? file.writeQueue.freeSize() : -1)));
+ }
+
+ if ((pollFd.events & POLLIN) != 0) {
+ if (file.readQueue != null && file.readQueue.size() != 0) {
+ pollFd.revents |= POLLIN;
+ }
+ if (file.isEndOfStream) {
+ pollFd.revents |= POLLHUP;
+ }
+ }
+
+ if ((pollFd.events & POLLOUT) != 0) {
+ if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+ pollFd.revents |= POLLERR;
+ }
+ if (file.writeQueue != null && file.writeQueue.freeSize() != 0) {
+ pollFd.revents |= POLLOUT;
+ }
+ }
+
+ if (pollFd.revents != 0) {
+ fdCount++;
+ }
+ }
+ return fdCount;
+ }
+
+ private int getNextFd(String func) throws IOException {
+ if (mFileNumberGen > 100000) {
+ throw newIOException(func, "Too many files open");
+ }
+
+ return mFileNumberGen++;
+ }
+
+ private static IOException newIOException(String func, String message) {
+ return new IOException(message + ", func=" + func);
+ }
+
+ public static void checkBoundaries(String func, byte[] buffer, int pos, int len)
+ throws IOException {
+ if (((buffer.length | pos | len) < 0 || pos > buffer.length - len)) {
+ throw newIOException(func, "Invalid array bounds");
+ }
+ }
+
+ private ParcelFileDescriptor newParcelFileDescriptor(int fdNumber) {
+ try {
+ return new ParcelFileDescriptor(newFileDescriptor(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private FileDescriptor newFileDescriptor(int fdNumber) {
+ try {
+ return FD_CONSTRUCTOR.newInstance(Integer.valueOf(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getFileDescriptorNumber(FileDescriptor fd) {
+ try {
+ return (Integer) FD_FIELD_DESCRIPTOR.get(fd);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void setFileDescriptorNumber(FileDescriptor fd, int fdNumber) {
+ try {
+ FD_FIELD_DESCRIPTOR.set(fd, Integer.valueOf(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String logStr(String message) {
+ return "[FakeOs " + (monotonicTimeMillis() - mStartTime) + "] " + message;
+ }
+
+ private class File {
+ final FileType type;
+ final CircularByteBuffer readQueue;
+ final CircularByteBuffer writeQueue;
+ final RateLimiter inboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+ final RateLimiter outboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+ String name;
+ int openCount = 1;
+ boolean isBlocking = true;
+ File sink;
+ File source;
+ boolean isEndOfStream;
+
+ File(FileType type, int readQueueSize, int writeQueueSize) {
+ this.type = type;
+ readQueue = (readQueueSize > 0 ? new CircularByteBuffer(readQueueSize) : null);
+ writeQueue = (writeQueueSize > 0 ? new CircularByteBuffer(writeQueueSize) : null);
+ }
+
+ void decreaseRefCount() {
+ if (openCount <= 0) {
+ throw new IllegalStateException();
+ }
+ openCount--;
+ }
+
+ void checkNonBlocking(String func) throws IOException {
+ if (isBlocking) {
+ throw newIOException(func, "File in blocking mode");
+ }
+ }
+ }
+
+ static {
+ try {
+ FD_CONSTRUCTOR = FileDescriptor.class.getDeclaredConstructor(int.class);
+ FD_CONSTRUCTOR.setAccessible(true);
+
+ Field descriptorIntField;
+ try {
+ descriptorIntField = FileDescriptor.class.getDeclaredField("descriptor");
+ } catch (NoSuchFieldException e) {
+ descriptorIntField = FileDescriptor.class.getDeclaredField("fd");
+ }
+ FD_FIELD_DESCRIPTOR = descriptorIntField;
+ FD_FIELD_DESCRIPTOR.setAccessible(true);
+
+ PFD_FIELD_DESCRIPTOR = ParcelFileDescriptor.class.getDeclaredField("mFd");
+ PFD_FIELD_DESCRIPTOR.setAccessible(true);
+
+ PFD_FIELD_GUARD = ParcelFileDescriptor.class.getDeclaredField("mGuard");
+ PFD_FIELD_GUARD.setAccessible(true);
+
+ CLOSE_GUARD_METHOD_CLOSE = Class.forName("dalvik.system.CloseGuard")
+ .getDeclaredMethod("close");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
new file mode 100644
index 0000000..d5cca0a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 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.async;
+
+import com.android.net.module.util.async.OsAccess;
+
+import java.util.Arrays;
+
+/**
+ * Limits the number of bytes processed to the given maximum of bytes per second.
+ *
+ * The limiter tracks the total for the past second, along with sums for each 10ms
+ * in the past second, allowing the total to be adjusted as the time passes.
+ */
+public final class RateLimiter {
+ private static final int PERIOD_DURATION_MS = 1000;
+ private static final int BUCKET_COUNT = 100;
+
+ public static final int BUCKET_DURATION_MS = PERIOD_DURATION_MS / BUCKET_COUNT;
+
+ private final OsAccess mOsAccess;
+ private final int[] mStatBuckets = new int[BUCKET_COUNT];
+ private int mMaxPerPeriodBytes;
+ private int mMaxPerBucketBytes;
+ private int mRecordedPeriodBytes;
+ private long mLastLimitTimestamp;
+ private int mLastRequestReduction;
+
+ public RateLimiter(OsAccess osAccess, int bytesPerSecond) {
+ mOsAccess = osAccess;
+ setBytesPerSecond(bytesPerSecond);
+ clear();
+ }
+
+ public int getBytesPerSecond() {
+ return mMaxPerPeriodBytes;
+ }
+
+ public void setBytesPerSecond(int bytesPerSecond) {
+ mMaxPerPeriodBytes = bytesPerSecond;
+ mMaxPerBucketBytes = Math.max(1, (mMaxPerPeriodBytes / BUCKET_COUNT) * 2);
+ }
+
+ public void clear() {
+ mLastLimitTimestamp = mOsAccess.monotonicTimeMillis();
+ mRecordedPeriodBytes = 0;
+ Arrays.fill(mStatBuckets, 0);
+ }
+
+ public static int limit(RateLimiter limiter1, RateLimiter limiter2, int requestedBytes) {
+ final long now = limiter1.mOsAccess.monotonicTimeMillis();
+ final int allowedCount = Math.min(limiter1.calculateLimit(now, requestedBytes),
+ limiter2.calculateLimit(now, requestedBytes));
+ limiter1.recordBytes(now, requestedBytes, allowedCount);
+ limiter2.recordBytes(now, requestedBytes, allowedCount);
+ return allowedCount;
+ }
+
+ public int limit(int requestedBytes) {
+ final long now = mOsAccess.monotonicTimeMillis();
+ final int allowedCount = calculateLimit(now, requestedBytes);
+ recordBytes(now, requestedBytes, allowedCount);
+ return allowedCount;
+ }
+
+ public int getLastRequestReduction() {
+ return mLastRequestReduction;
+ }
+
+ public boolean acceptAllOrNone(int requestedBytes) {
+ final long now = mOsAccess.monotonicTimeMillis();
+ final int allowedCount = calculateLimit(now, requestedBytes);
+ if (allowedCount < requestedBytes) {
+ return false;
+ }
+ recordBytes(now, requestedBytes, allowedCount);
+ return true;
+ }
+
+ private int calculateLimit(long now, int requestedBytes) {
+ // First remove all stale bucket data and adjust the total.
+ final long currentBucketAbsIdx = now / BUCKET_DURATION_MS;
+ final long staleCutoffIdx = currentBucketAbsIdx - BUCKET_COUNT;
+ for (long i = mLastLimitTimestamp / BUCKET_DURATION_MS; i < staleCutoffIdx; i++) {
+ final int idx = (int) (i % BUCKET_COUNT);
+ mRecordedPeriodBytes -= mStatBuckets[idx];
+ mStatBuckets[idx] = 0;
+ }
+
+ final int bucketIdx = (int) (currentBucketAbsIdx % BUCKET_COUNT);
+ final int maxAllowed = Math.min(mMaxPerPeriodBytes - mRecordedPeriodBytes,
+ Math.min(mMaxPerBucketBytes - mStatBuckets[bucketIdx], requestedBytes));
+ return Math.max(0, maxAllowed);
+ }
+
+ private void recordBytes(long now, int requestedBytes, int actualBytes) {
+ mStatBuckets[(int) ((now / BUCKET_DURATION_MS) % BUCKET_COUNT)] += actualBytes;
+ mRecordedPeriodBytes += actualBytes;
+ mLastRequestReduction = requestedBytes - actualBytes;
+ mLastLimitTimestamp = now;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{max=");
+ sb.append(mMaxPerPeriodBytes);
+ sb.append(",max_bucket=");
+ sb.append(mMaxPerBucketBytes);
+ sb.append(",total=");
+ sb.append(mRecordedPeriodBytes);
+ sb.append(",last_red=");
+ sb.append(mLastRequestReduction);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java b/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java
new file mode 100644
index 0000000..4bf5527
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.async;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+
+public class ReadableDataAnswer implements Answer {
+ private final ArrayList<byte[]> mBuffers = new ArrayList<>();
+ private int mBufferPos;
+
+ public ReadableDataAnswer(byte[] ... buffers) {
+ for (byte[] buffer : buffers) {
+ addBuffer(buffer);
+ }
+ }
+
+ public void addBuffer(byte[] buffer) {
+ if (buffer.length != 0) {
+ mBuffers.add(buffer);
+ }
+ }
+
+ public int getRemainingSize() {
+ int totalSize = 0;
+ for (byte[] buffer : mBuffers) {
+ totalSize += buffer.length;
+ }
+ return totalSize - mBufferPos;
+ }
+
+ private void cleanupBuffers() {
+ if (!mBuffers.isEmpty() && mBufferPos == mBuffers.get(0).length) {
+ mBuffers.remove(0);
+ mBufferPos = 0;
+ }
+ }
+
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ cleanupBuffers();
+
+ if (mBuffers.isEmpty()) {
+ return Integer.valueOf(0);
+ }
+
+ byte[] src = mBuffers.get(0);
+
+ byte[] dst = invocation.<byte[]>getArgument(0);
+ int dstPos = invocation.<Integer>getArgument(1);
+ int dstLen = invocation.<Integer>getArgument(2);
+
+ int copyLen = Math.min(dstLen, src.length - mBufferPos);
+ System.arraycopy(src, mBufferPos, dst, dstPos, copyLen);
+ mBufferPos += copyLen;
+
+ cleanupBuffers();
+ return Integer.valueOf(copyLen);
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk30.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk30.kt
new file mode 100644
index 0000000..843c41e
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk30.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 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.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk30 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk30(val reason: String)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt
new file mode 100644
index 0000000..be0103d
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk31.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk31 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk31(val reason: String)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
new file mode 100644
index 0000000..5af890f
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk33 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk33(val reason: String)
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
new file mode 100644
index 0000000..3fc74aa
--- /dev/null
+++ b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -0,0 +1,116 @@
+/*
+ * 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 com.android.ddmlib.testrunner.TestResult
+import com.android.tradefed.config.Option
+import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.result.CollectingTestListener
+import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
+import com.android.tradefed.targetprep.BaseTargetPreparer
+import com.android.tradefed.targetprep.TargetSetupError
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller
+
+private const val CONNECTIVITY_CHECKER_APK = "ConnectivityTestPreparer.apk"
+private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitypreparer"
+private const val CONNECTIVITY_CHECK_CLASS = "$CONNECTIVITY_PKG_NAME.ConnectivityCheckTest"
+// As per the <instrumentation> defined in the checker manifest
+private const val CONNECTIVITY_CHECK_RUNNER_NAME = "androidx.test.runner.AndroidJUnitRunner"
+private const val IGNORE_CONN_CHECK_OPTION = "ignore-connectivity-check"
+
+/**
+ * A target preparer that sets up and verifies a device for connectivity tests.
+ *
+ * For quick and dirty local testing, the connectivity check can be disabled by running tests with
+ * "atest -- \
+ * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-connectivity-check:true".
+ */
+open class ConnectivityTestTargetPreparer : BaseTargetPreparer() {
+ private val installer = SuiteApkInstaller()
+
+ @Option(name = IGNORE_CONN_CHECK_OPTION,
+ description = "Disables the check for mobile data and wifi")
+ private var ignoreConnectivityCheck = false
+
+ override fun setUp(testInformation: TestInformation) {
+ if (isDisabled) return
+ disableGmsUpdate(testInformation)
+ runPreparerApk(testInformation)
+ }
+
+ private fun runPreparerApk(testInformation: TestInformation) {
+ installer.setCleanApk(true)
+ installer.addTestFileName(CONNECTIVITY_CHECKER_APK)
+ installer.setShouldGrantPermission(true)
+ installer.setUp(testInformation)
+
+ val runner = DefaultRemoteAndroidTestRunner(
+ CONNECTIVITY_PKG_NAME,
+ CONNECTIVITY_CHECK_RUNNER_NAME,
+ testInformation.device.iDevice)
+ runner.runOptions = "--no-hidden-api-checks"
+
+ val receiver = CollectingTestListener()
+ if (!testInformation.device.runInstrumentationTests(runner, receiver)) {
+ throw TargetSetupError("Device state check failed to complete",
+ testInformation.device.deviceDescriptor)
+ }
+
+ val runResult = receiver.currentRunResults
+ if (runResult.isRunFailure) {
+ throw TargetSetupError("Failed to check device state before the test: " +
+ runResult.runFailureMessage, testInformation.device.deviceDescriptor)
+ }
+
+ val ignoredTestClasses = mutableSetOf<String>()
+ if (ignoreConnectivityCheck) {
+ ignoredTestClasses.add(CONNECTIVITY_CHECK_CLASS)
+ }
+
+ val errorMsg = runResult.testResults.mapNotNull { (testDescription, testResult) ->
+ if (TestResult.TestStatus.FAILURE != testResult.status ||
+ ignoredTestClasses.contains(testDescription.className)) {
+ null
+ } else {
+ "$testDescription: ${testResult.stackTrace}"
+ }
+ }.joinToString("\n")
+ if (errorMsg.isBlank()) return
+
+ throw TargetSetupError("Device setup checks failed. Check the test bench: \n$errorMsg",
+ testInformation.device.deviceDescriptor)
+ }
+
+ private fun disableGmsUpdate(testInformation: TestInformation) {
+ // This will be a no-op on devices without root (su) or not using gservices, but that's OK.
+ testInformation.device.executeShellCommand("su 0 am broadcast " +
+ "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
+ "-e finsky.play_services_auto_update_enabled false")
+ }
+
+ private fun clearGmsUpdateOverride(testInformation: TestInformation) {
+ testInformation.device.executeShellCommand("su 0 am broadcast " +
+ "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
+ "--esn finsky.play_services_auto_update_enabled")
+ }
+
+ override fun tearDown(testInformation: TestInformation, e: Throwable?) {
+ if (isTearDownDisabled) return
+ installer.tearDown(testInformation, e)
+ clearGmsUpdateOverride(testInformation)
+ }
+}
diff --git a/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt
new file mode 100644
index 0000000..63f05a6
--- /dev/null
+++ b/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt
@@ -0,0 +1,61 @@
+/*
+ * 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 com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.targetprep.BaseTargetPreparer
+
+/**
+ * A target preparer that disables DeviceConfig sync while running a test.
+ *
+ * Without this preparer, tests that rely on stable values of DeviceConfig flags, for example to
+ * test behavior when setting the flag and resetting it afterwards, may flake as the flags may
+ * be synced with remote servers during the test.
+ */
+class DisableConfigSyncTargetPreparer : BaseTargetPreparer() {
+ private var syncDisabledOriginalValue = "none"
+
+ override fun setUp(testInfo: TestInformation) {
+ if (isDisabled) return
+ syncDisabledOriginalValue = readSyncDisabledOriginalValue(testInfo)
+
+ // The setter is the same in current and legacy S versions
+ testInfo.exec("cmd device_config set_sync_disabled_for_tests until_reboot")
+ }
+
+ override fun tearDown(testInfo: TestInformation, e: Throwable?) {
+ if (isTearDownDisabled) return
+ // May fail harmlessly if called before S
+ testInfo.exec("cmd device_config set_sync_disabled_for_tests $syncDisabledOriginalValue")
+ }
+
+ private fun readSyncDisabledOriginalValue(testInfo: TestInformation): String {
+ return when (val reply = testInfo.exec("cmd device_config get_sync_disabled_for_tests")) {
+ "until_reboot", "persistent", "none" -> reply
+ // Reply does not match known modes, try legacy commands used on S and some T builds
+ else -> when (testInfo.exec("cmd device_config is_sync_disabled_for_tests")) {
+ // The legacy command just said "true" for "until_reboot" or "persistent". There is
+ // no way to know which one was used, so just reset to "until_reboot" to be
+ // conservative.
+ "true" -> "until_reboot"
+ else -> "none"
+ }
+ }
+ }
+}
+
+private fun TestInformation.exec(cmd: String) = this.device.executeShellCommand(cmd)
\ No newline at end of file
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..f24e4f1
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/net/module/util/TrackRecord.kt
@@ -0,0 +1,311 @@
+/*
+ * 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
+
+ // A special mark used to track the start of the last poll() operation.
+ private var pollMark = 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
+ pollMark = 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()
+ pollMark = readHead
+ 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 a list of events that were observed since the last time poll() was called on this
+ * ReadHead.
+ *
+ * @return list of events since poll() was called.
+ */
+ fun backtrace(): List<E> {
+ val stamp = slock.tryReadLock()
+ if (0L == stamp) concurrentAccessDetected()
+
+ try {
+ lock.withLock {
+ return ArrayList(subList(pollMark, mark))
+ }
+ } finally {
+ slock.unlockRead(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/Cleanup.kt b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
new file mode 100644
index 0000000..9f28234
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("Cleanup")
+
+package com.android.testutils
+
+import com.android.testutils.FunctionalUtils.ThrowingRunnable
+import com.android.testutils.FunctionalUtils.ThrowingSupplier
+import javax.annotation.CheckReturnValue
+
+/**
+ * Utility to do cleanup in tests without replacing exceptions with those from a finally block.
+ *
+ * This utility is meant for tests that want to do cleanup after they execute their test
+ * logic, whether the test fails (and throws) or not.
+ *
+ * The usual way of doing this is to have a try{}finally{} block and put cleanup in finally{}.
+ * However, if any code in finally{} throws, the exception thrown in finally{} is thrown before
+ * any thrown in try{} ; that means errors reported from tests are from finally{} even if they
+ * have been caused by errors in try{}. This is unhelpful in tests, because it results in a
+ * stacktrace for a symptom rather than a stacktrace for a cause.
+ *
+ * To alleviate this, tests are encouraged to make sure the code in finally{} can't throw, or
+ * that the code in try{} can't cause it to fail. This is not always realistic ; not only does
+ * it require the developer thinks about complex interactions of code, test code often relies
+ * on bricks provided by other teams, not controlled by the team writing the test, which may
+ * start throwing with an update (see b/198998862 for an example).
+ *
+ * This utility allows a different approach : it offers a new construct, tryTest{}cleanup{} similar
+ * to try{}finally{}, but that will always throw the first exception that happens. In other words,
+ * if only tryTest{} throws or only cleanup{} throws, that exception will be thrown, but contrary
+ * to the standard try{}finally{}, if both throws, the construct throws the exception that happened
+ * in tryTest{} rather than the one that happened in cleanup{}.
+ *
+ * Kotlin usage is as try{}finally{}, but with multiple finally{} blocks :
+ * tryTest {
+ * testing code
+ * } cleanupStep {
+ * cleanup code 1
+ * } cleanupStep {
+ * cleanup code 2
+ * } cleanup {
+ * cleanup code 3
+ * }
+ * Catch blocks can be added with the following syntax :
+ * tryTest {
+ * testing code
+ * }.catch<ExceptionType> { it ->
+ * do something to it
+ * }
+ *
+ * Java doesn't allow this kind of syntax, so instead a function taking lambdas is provided.
+ * testAndCleanup(() -> {
+ * testing code
+ * }, () -> {
+ * cleanup code 1
+ * }, () -> {
+ * cleanup code 2
+ * });
+ */
+
+@CheckReturnValue
+fun <T> tryTest(block: () -> T) = TryExpr(
+ try {
+ Result.success(block())
+ } catch (e: Throwable) {
+ Result.failure(e)
+ })
+
+// Some downstream branches have an older kotlin that doesn't know about value classes.
+// TODO : Change this to "value class" when aosp no longer merges into such branches.
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class TryExpr<T>(val result: Result<T>) {
+ inline infix fun <reified E : Throwable> catch(block: (E) -> T): TryExpr<T> {
+ val originalException = result.exceptionOrNull()
+ if (originalException !is E) return this
+ return TryExpr(try {
+ Result.success(block(originalException))
+ } catch (e: Throwable) {
+ Result.failure(e)
+ })
+ }
+
+ @CheckReturnValue
+ inline infix fun cleanupStep(block: () -> Unit): TryExpr<T> {
+ try {
+ block()
+ } catch (e: Throwable) {
+ val originalException = result.exceptionOrNull()
+ return TryExpr(if (null == originalException) {
+ Result.failure(e)
+ } else {
+ originalException.addSuppressed(e)
+ Result.failure(originalException)
+ })
+ }
+ return this
+ }
+
+ inline infix fun cleanup(block: () -> Unit): T = cleanupStep(block).result.getOrThrow()
+}
+
+// Java support
+fun <T> testAndCleanup(tryBlock: ThrowingSupplier<T>, vararg cleanupBlock: ThrowingRunnable): T {
+ return cleanupBlock.fold(tryTest { tryBlock.get() }) { previousExpr, nextCleanup ->
+ previousExpr.cleanupStep { nextCleanup.run() }
+ }.cleanup {}
+}
+fun testAndCleanup(tryBlock: ThrowingRunnable, vararg cleanupBlock: ThrowingRunnable) {
+ return testAndCleanup(ThrowingSupplier { tryBlock.run() }, *cleanupBlock)
+}
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/ConnectivityModuleTest.kt b/staticlibs/testutils/hostdevice/com/android/testutils/ConnectivityModuleTest.kt
new file mode 100644
index 0000000..ec485fe
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/ConnectivityModuleTest.kt
@@ -0,0 +1,27 @@
+/*
+ * 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
+
+/**
+ * Indicates that the test covers functionality that was rolled out in a connectivity module update.
+ *
+ * Annotated MTS tests will typically only be run in Connectivity/Tethering module MTS, and not when
+ * only other modules (such as NetworkStack) have been updated.
+ * Annotated CTS tests will always be run, as the Connectivity module should be at least newer than
+ * the CTS suite.
+ */
+annotation class ConnectivityModuleTest
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt b/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt
new file mode 100644
index 0000000..9e97d51
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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
+
+/**
+ * Indicates that the test covers functionality that was rolled out in a resolv module update.
+ */
+annotation class DnsResolverModuleTest
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/FunctionalUtils.java b/staticlibs/testutils/hostdevice/com/android/testutils/FunctionalUtils.java
new file mode 100644
index 0000000..da36e4d
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/FunctionalUtils.java
@@ -0,0 +1,99 @@
+/*
+ * 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 FunctionalUtils {
+ /**
+ * 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) {
+ }
+ };
+ }
+
+ // Java has Function<T, R> and BiFunction<T, U, V> but nothing for higher-arity functions.
+ // Function3 is what Kotlin and Scala use (they also have higher-arity variants, with
+ // FunctionN taking N arguments, as the JVM does not have variadic formal parameters)
+ /**
+ * A function with three arguments.
+ * @param <TArg1> Type of the first argument
+ * @param <TArg2> Type of the second argument
+ * @param <TArg3> Type of the third argument
+ * @param <TResult> Type of the return value
+ */
+ public interface Function3<TArg1, TArg2, TArg3, TResult> {
+ /**
+ * Apply the function to the arguments
+ */
+ TResult apply(TArg1 a1, TArg2 a2, TArg3 a3);
+ }
+}
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..1883387
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.FunctionalUtils.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 <T> assertSameElements(expected: List<T>, actual: List<T>) {
+ val expectedSet: HashSet<T> = HashSet(expected)
+ assertEquals(expectedSet.size, expected.size, "expected list contains duplicates")
+ val actualSet: HashSet<T> = HashSet(actual)
+ assertEquals(actualSet.size, actual.size, "actual list contains duplicates")
+ assertEquals(expectedSet, actualSet)
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/NetworkStackModuleTest.kt b/staticlibs/testutils/hostdevice/com/android/testutils/NetworkStackModuleTest.kt
new file mode 100644
index 0000000..fe312a0
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/NetworkStackModuleTest.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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
+
+/**
+ * Indicates that the test covers functionality that was rolled out in a NetworkStack module update.
+ */
+annotation class NetworkStackModuleTest
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..1bb6d68
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt
@@ -0,0 +1,148 @@
+/*
+ * 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
+
+// Some of the below constants are duplicated with NetworkStackConstants, but this is a hostdevice
+// library usable for host-side tests, so device-side utils are not usable, and there is no
+// host-side non-test library to host common constants.
+private const val ETHER_TYPE_OFFSET = 12
+private const val ETHER_HEADER_LENGTH = 14
+private const val IPV4_PROTOCOL_OFFSET = ETHER_HEADER_LENGTH + 9
+private const val IPV6_PROTOCOL_OFFSET = ETHER_HEADER_LENGTH + 6
+private const val IPV4_CHECKSUM_OFFSET = ETHER_HEADER_LENGTH + 10
+private const val IPV4_DST_OFFSET = ETHER_HEADER_LENGTH + 16
+private const val IPV4_HEADER_LENGTH = 20
+private const val IPV6_HEADER_LENGTH = 40
+private const val IPV4_PAYLOAD_OFFSET = ETHER_HEADER_LENGTH + IPV4_HEADER_LENGTH
+private const val IPV6_PAYLOAD_OFFSET = ETHER_HEADER_LENGTH + IPV6_HEADER_LENGTH
+private const val UDP_HEADER_LENGTH = 8
+private const val BOOTP_OFFSET = IPV4_PAYLOAD_OFFSET + UDP_HEADER_LENGTH
+private const val BOOTP_TID_OFFSET = BOOTP_OFFSET + 4
+private const val BOOTP_CLIENT_MAC_OFFSET = BOOTP_OFFSET + 28
+private const val DHCP_OPTIONS_OFFSET = BOOTP_OFFSET + 240
+private const val ARP_OPCODE_OFFSET = ETHER_HEADER_LENGTH + 6
+
+/**
+ * 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] }
+}
+
+private class UdpPortFilter(
+ private val udpOffset: Int,
+ private val src: Short?,
+ private val dst: Short?
+) : Predicate<ByteArray> {
+ override fun test(t: ByteArray): Boolean {
+ if (src != null && !OffsetFilter(udpOffset,
+ src.toInt().ushr(8).toByte(), src.toByte()).test(t)) {
+ return false
+ }
+
+ if (dst != null && !OffsetFilter(udpOffset + 2,
+ dst.toInt().ushr(8).toByte(), dst.toByte()).test(t)) {
+ return false
+ }
+ return true
+ }
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped packets that contain an UDP over IPv4 datagram.
+ */
+class IPv4UdpFilter @JvmOverloads constructor(
+ srcPort: Short? = null,
+ dstPort: Short? = null
+) : Predicate<ByteArray> {
+ private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x08, 0x00 /* IPv4 */).and(
+ OffsetFilter(IPV4_PROTOCOL_OFFSET, 17 /* UDP */)).and(
+ UdpPortFilter(IPV4_PAYLOAD_OFFSET, srcPort, dstPort))
+ override fun test(t: ByteArray) = impl.test(t)
+}
+
+/**
+ * A [Predicate] that matches ethernet-encapped packets that contain an UDP over IPv6 datagram.
+ */
+class IPv6UdpFilter @JvmOverloads constructor(
+ srcPort: Short? = null,
+ dstPort: Short? = null
+) : Predicate<ByteArray> {
+ private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x86.toByte(), 0xdd.toByte() /* IPv6 */).and(
+ OffsetFilter(IPV6_PROTOCOL_OFFSET, 17 /* UDP */)).and(
+ UdpPortFilter(IPV6_PAYLOAD_OFFSET, srcPort, dstPort))
+ 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(srcPort = 68, dstPort = 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/SkipMainlinePresubmit.kt b/staticlibs/testutils/hostdevice/com/android/testutils/SkipMainlinePresubmit.kt
new file mode 100644
index 0000000..5952365
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/SkipMainlinePresubmit.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 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 limitations that prevent a test from being
+ * executed in presubmit on older builds.
+ */
+annotation class SkipMainlinePresubmit(val reason: String)
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