[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: 403714c1b3 -s ours

am skip reason: contains skip directive

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/28767939

Change-Id: Ib1f4b8e41953fca086f067bed4916e0d3b9ebc11
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Cronet/tests/OWNERS b/Cronet/tests/OWNERS
deleted file mode 100644
index a35a789..0000000
--- a/Cronet/tests/OWNERS
+++ /dev/null
@@ -1,8 +0,0 @@
-# Bug component: 31808
-
-set noparent
-file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking_xts
-
-# TODO: Temp ownership to develop cronet CTS
-colibie@google.com #{LAST_RESORT_SUGGESTION}
-prohr@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
deleted file mode 100644
index edeb0b3..0000000
--- a/Cronet/tests/common/Android.bp
+++ /dev/null
@@ -1,48 +0,0 @@
-// 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.
-//
-
-// Tests in this folder are included both in unit tests and CTS.
-// They must be fast and stable, and exercise public or test APIs.
-
-package {
-    default_team: "trendy_team_fwk_core_networking",
-    // See: http://go/android-license-faq
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-// TODO: Consider merging with ConnectivityCoverageTests which is a collection of all
-// Connectivity tests being used for coverage. This will depend on how far we decide to
-// go with merging NetHttp and Tethering targets.
-android_test {
-    name: "NetHttpCoverageTests",
-    enforce_default_target_sdk_version: true,
-    min_sdk_version: "30",
-    test_suites: [
-        "general-tests",
-        "mts-tethering",
-    ],
-    static_libs: [
-        "modules-utils-native-coverage-listener",
-        "CtsNetHttpTestsLib",
-        "NetHttpTestsLibPreJarJar",
-    ],
-    jarjar_rules: ":net-http-test-jarjar-rules",
-    compile_multilib: "both", // Include both the 32 and 64 bit versions
-    jni_libs: [
-        "cronet_aml_components_cronet_android_cronet_tests__testing",
-        "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing",
-    ],
-    data: [":cronet_javatests_resources"],
-}
diff --git a/Cronet/tests/common/AndroidManifest.xml b/Cronet/tests/common/AndroidManifest.xml
deleted file mode 100644
index b00fc90..0000000
--- a/Cronet/tests/common/AndroidManifest.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          xmlns:tools="http://schemas.android.com/tools"
-          package="com.android.net.http.tests.coverage">
-
-    <!-- NetHttpCoverageTests combines CtsNetHttpTestCases and NetHttpTests targets,
-     so permissions and others are declared in their respective manifests -->
-    <application tools:replace="android:label"
-                 android:label="NetHttp coverage tests">
-        <uses-library android:name="android.test.runner" />
-    </application>
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.net.http.tests.coverage"
-                     android:label="NetHttp coverage tests">
-    </instrumentation>
-</manifest>
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
deleted file mode 100644
index bded8fb..0000000
--- a/Cronet/tests/common/AndroidTest.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<!--
-  ~ 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.
-  -->
-<configuration description="Runs coverage tests for NetHttp">
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="test-file-name" value="NetHttpCoverageTests.apk" />
-        <option name="install-arg" value="-t" />
-    </target_preparer>
-    <option name="test-tag" value="NetHttpCoverageTests" />
-
-    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
-        <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" />
-        <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" />
-    </target_preparer>
-    <!-- Tethering/Connectivity is a SDK 30+ module -->
-    <!-- TODO Switch back to Sdk30 when b/270049141 is fixed -->
-    <object type="module_controller"
-            class="com.android.tradefed.testtype.suite.module.Sdk31ModuleController" />
-    <option name="config-descriptor:metadata" key="mainline-param"
-            value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="com.android.net.http.tests.coverage" />
-        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
-        <!-- b/298380508 -->
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
-        <!-- b/316571753 -->
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
-        <!-- b/316567693 -->
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
-        <!-- b/316559294 -->
-        <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
-        <!-- b/316559294 -->
-        <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
-        <!-- b/316554711-->
-        <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" /> 
-        <!-- b/316550794 -->
-        <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
-        <option name="hidden-api-checks" value="false"/>
-        <option name="isolated-storage" value="false"/>
-        <option
-            name="device-listeners"
-            value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
-    </test>
-</configuration>
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
deleted file mode 100644
index 92b73d9..0000000
--- a/Cronet/tests/cts/Android.bp
+++ /dev/null
@@ -1,71 +0,0 @@
-//
-// 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 {
-    default_team: "trendy_team_fwk_core_networking",
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_library {
-    name: "CtsNetHttpTestsLib",
-    defaults: [
-        "cts_defaults",
-    ],
-    sdk_version: "test_current",
-    min_sdk_version: "30",
-    srcs: [
-        "src/**/*.java",
-        "src/**/*.kt",
-    ],
-    static_libs: [
-        "androidx.test.ext.junit",
-        "ctstestrunner-axt",
-        "ctstestserver",
-        "hamcrest-library",
-        "junit",
-        "kotlin-test",
-        "mockito-target",
-        "net-tests-utils",
-        "truth",
-    ],
-    libs: [
-        "android.test.base",
-        "androidx.annotation_annotation",
-        "framework-connectivity",
-        "org.apache.http.legacy",
-    ],
-    lint: {
-        test: true,
-    },
-}
-
-android_test {
-    name: "CtsNetHttpTestCases",
-    defaults: [
-        "cts_defaults",
-    ],
-    enforce_default_target_sdk_version: true,
-    sdk_version: "test_current",
-    min_sdk_version: "30",
-    static_libs: ["CtsNetHttpTestsLib"],
-    // Tag this as a cts test artifact
-    test_suites: [
-        "cts",
-        "general-tests",
-        "mts-tethering",
-        "mcts-tethering",
-    ],
-}
diff --git a/Cronet/tests/cts/AndroidManifest.xml b/Cronet/tests/cts/AndroidManifest.xml
deleted file mode 100644
index 26900b2..0000000
--- a/Cronet/tests/cts/AndroidManifest.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * 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.
- */
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.net.http.cts">
-
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-
-    <application android:networkSecurityConfig="@xml/network_security_config">
-        <uses-library android:name="android.test.runner"/>
-    </application>
-
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="android.net.http.cts"
-        android:label="CTS tests of android.net.http">
-    </instrumentation>
-</manifest>
diff --git a/Cronet/tests/cts/AndroidTest.xml b/Cronet/tests/cts/AndroidTest.xml
deleted file mode 100644
index e0421fd..0000000
--- a/Cronet/tests/cts/AndroidTest.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-<configuration description="Config for CTS Cronet test cases">
-    <option name="test-suite-tag" value="cts" />
-    <option name="config-descriptor:metadata" key="component" value="networking" />
-    <!-- Instant apps cannot create sockets. See b/264248246 -->
-    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
-    <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
-    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="CtsNetHttpTestCases.apk" />
-    </target_preparer>
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="android.net.http.cts" />
-        <option name="runtime-hint" value="10s" />
-    </test>
-
-    <!-- Only run CtsNetHttpTestCases in MTS if the Tethering Mainline module is installed. -->
-    <object type="module_controller"
-            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
-        <option name="mainline-module-package-name" value="com.google.android.tethering" />
-    </object>
-</configuration>
diff --git a/Cronet/tests/cts/res/xml/network_security_config.xml b/Cronet/tests/cts/res/xml/network_security_config.xml
deleted file mode 100644
index 7d7530b..0000000
--- a/Cronet/tests/cts/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  ~ 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.
-  -->
-
-<network-security-config>
-    <domain-config cleartextTrafficPermitted="true">
-        <domain includeSubdomains="true">localhost</domain>
-    </domain-config>
-</network-security-config>
\ No newline at end of file
diff --git a/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt b/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
deleted file mode 100644
index 464862d..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * 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 android.net.http.cts
-
-import android.content.Context
-import android.net.http.BidirectionalStream
-import android.net.http.HttpEngine
-import android.net.http.cts.util.TestBidirectionalStreamCallback
-import android.net.http.cts.util.TestBidirectionalStreamCallback.ResponseStep
-import android.net.http.cts.util.assumeOKStatusCode
-import android.net.http.cts.util.skipIfNoInternetConnection
-import android.os.Build
-import androidx.test.core.app.ApplicationProvider
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.SkipPresubmit
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import org.hamcrest.MatcherAssert
-import org.hamcrest.Matchers
-import org.junit.After
-import org.junit.AssumptionViolatedException
-import org.junit.Before
-import org.junit.runner.RunWith
-
-private const val URL = "https://source.android.com"
-
-/**
- * This tests uses a non-hermetic server. Instead of asserting, assume the next callback. This way,
- * if the request were to fail, the test would just be skipped instead of failing.
- */
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class BidirectionalStreamTest {
-    private val context: Context = ApplicationProvider.getApplicationContext()
-    private val callback = TestBidirectionalStreamCallback()
-    private val httpEngine = HttpEngine.Builder(context).build()
-    private var stream: BidirectionalStream? = null
-
-    @Before
-    fun setUp() {
-        skipIfNoInternetConnection(context)
-    }
-
-    @After
-    @Throws(Exception::class)
-    fun tearDown() {
-        httpEngine.shutdown()
-    }
-
-    private fun createBidirectionalStreamBuilder(url: String): BidirectionalStream.Builder {
-        return httpEngine.newBidirectionalStreamBuilder(url, callback.executor, callback)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    @SkipPresubmit(reason = "b/293141085 Confirm non-flaky and move to presubmit after SLO")
-    fun testBidirectionalStream_GetStream_CompletesSuccessfully() {
-        stream = createBidirectionalStreamBuilder(URL).setHttpMethod("GET").build()
-        stream!!.start()
-        // We call to a real server and hence the server may not be reachable, cancel this stream
-        // and rethrow the exception before tearDown,
-        // otherwise shutdown would fail with active request error.
-        try {
-            callback.assumeCallback(ResponseStep.ON_SUCCEEDED)
-        } catch (e: AssumptionViolatedException) {
-            stream!!.cancel()
-            callback.blockForDone()
-            throw e
-        }
-
-        val info = callback.mResponseInfo
-        assumeOKStatusCode(info)
-        MatcherAssert.assertThat(
-            "Received byte count must be > 0", info.receivedByteCount, Matchers.greaterThan(0L))
-        assertEquals("h2", info.negotiatedProtocol)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_getHttpMethod() {
-        val builder = createBidirectionalStreamBuilder(URL)
-        val method = "GET"
-
-        builder.setHttpMethod(method)
-        stream = builder.build()
-        assertThat(stream!!.getHttpMethod()).isEqualTo(method)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_hasTrafficStatsTag() {
-        val builder = createBidirectionalStreamBuilder(URL)
-
-        builder.setTrafficStatsTag(10)
-        stream = builder.build()
-        assertThat(stream!!.hasTrafficStatsTag()).isTrue()
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_getTrafficStatsTag() {
-        val builder = createBidirectionalStreamBuilder(URL)
-        val trafficStatsTag = 10
-
-        builder.setTrafficStatsTag(trafficStatsTag)
-        stream = builder.build()
-        assertThat(stream!!.getTrafficStatsTag()).isEqualTo(trafficStatsTag)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_hasTrafficStatsUid() {
-        val builder = createBidirectionalStreamBuilder(URL)
-
-        builder.setTrafficStatsUid(10)
-        stream = builder.build()
-        assertThat(stream!!.hasTrafficStatsUid()).isTrue()
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_getTrafficStatsUid() {
-        val builder = createBidirectionalStreamBuilder(URL)
-        val trafficStatsUid = 10
-
-        builder.setTrafficStatsUid(trafficStatsUid)
-        stream = builder.build()
-        assertThat(stream!!.getTrafficStatsUid()).isEqualTo(trafficStatsUid)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_getHeaders_asList() {
-        val builder = createBidirectionalStreamBuilder(URL)
-        val expectedHeaders = mapOf(
-          "Authorization" to "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
-          "Max-Forwards" to "10",
-          "X-Client-Data" to "random custom header content").entries.toList()
-
-        for (header in expectedHeaders) {
-            builder.addHeader(header.key, header.value)
-        }
-
-        stream = builder.build()
-        assertThat(stream!!.getHeaders().getAsList()).containsAtLeastElementsIn(expectedHeaders)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_getHeaders_asMap() {
-        val builder = createBidirectionalStreamBuilder(URL)
-        val expectedHeaders = mapOf(
-          "Authorization" to listOf("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="),
-          "Max-Forwards" to listOf("10"),
-          "X-Client-Data" to listOf("random custom header content"))
-
-        for (header in expectedHeaders) {
-            builder.addHeader(header.key, header.value.get(0))
-        }
-
-        stream = builder.build()
-        assertThat(stream!!.getHeaders().getAsMap()).containsAtLeastEntriesIn(expectedHeaders)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_getPriority() {
-        val builder = createBidirectionalStreamBuilder(URL)
-        val priority = BidirectionalStream.STREAM_PRIORITY_LOW
-
-        builder.setPriority(priority)
-        stream = builder.build()
-        assertThat(stream!!.getPriority()).isEqualTo(priority)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun testBidirectionalStream_isDelayRequestHeadersUntilFirstFlushEnabled() {
-        val builder = createBidirectionalStreamBuilder(URL)
-
-        builder.setDelayRequestHeadersUntilFirstFlushEnabled(true)
-        stream = builder.build()
-        assertThat(stream!!.isDelayRequestHeadersUntilFirstFlushEnabled()).isTrue()
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt b/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt
deleted file mode 100644
index 1405ed9..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 android.net.http.cts
-
-import android.content.Context
-import android.net.http.CallbackException
-import android.net.http.HttpEngine
-import android.net.http.cts.util.HttpCtsTestServer
-import android.net.http.cts.util.TestUrlRequestCallback
-import android.net.http.cts.util.TestUrlRequestCallback.FailureType
-import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep
-import android.os.Build
-import androidx.test.core.app.ApplicationProvider
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertIs
-import kotlin.test.assertSame
-import kotlin.test.assertTrue
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class CallbackExceptionTest {
-
-    @Test
-    fun testCallbackException_returnsInputParameters() {
-        val message = "failed"
-        val cause = Throwable("exception")
-        val callbackException = object : CallbackException(message, cause) {}
-
-        assertEquals(message, callbackException.message)
-        assertSame(cause, callbackException.cause)
-    }
-
-    @Test
-    fun testCallbackException_thrownFromUrlRequest() {
-        val context: Context = ApplicationProvider.getApplicationContext()
-        val server = HttpCtsTestServer(context)
-        val httpEngine = HttpEngine.Builder(context).build()
-        val callback = TestUrlRequestCallback()
-        callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED)
-        val request = httpEngine
-            .newUrlRequestBuilder(server.successUrl, callback.executor, callback)
-            .build()
-
-        request.start()
-        callback.blockForDone()
-
-        assertTrue(request.isDone)
-        assertIs<CallbackException>(callback.mError)
-        server.shutdown()
-        httpEngine.shutdown()
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt b/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt
deleted file mode 100644
index 10c7f3c..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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 android.net.http.cts
-
-import android.net.http.ConnectionMigrationOptions
-import android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED
-import android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_UNSPECIFIED
-import android.os.Build
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class ConnectionMigrationOptionsTest {
-
-    @Test
-    fun testConnectionMigrationOptions_defaultValues() {
-        val options =
-                ConnectionMigrationOptions.Builder().build()
-
-        assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.allowNonDefaultNetworkUsage)
-        assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.defaultNetworkMigration)
-        assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.pathDegradationMigration)
-    }
-
-    @Test
-    fun testConnectionMigrationOptions_enableDefaultNetworkMigration_returnSetValue() {
-        val options =
-            ConnectionMigrationOptions.Builder()
-                    .setDefaultNetworkMigration(MIGRATION_OPTION_ENABLED)
-                    .build()
-
-        assertEquals(MIGRATION_OPTION_ENABLED, options.defaultNetworkMigration)
-    }
-
-    @Test
-    fun testConnectionMigrationOptions_enablePathDegradationMigration_returnSetValue() {
-        val options =
-            ConnectionMigrationOptions.Builder()
-                    .setPathDegradationMigration(MIGRATION_OPTION_ENABLED)
-                    .build()
-
-        assertEquals(MIGRATION_OPTION_ENABLED, options.pathDegradationMigration)
-    }
-
-    @Test
-    fun testConnectionMigrationOptions_allowNonDefaultNetworkUsage_returnSetValue() {
-        val options =
-                ConnectionMigrationOptions.Builder()
-                        .setAllowNonDefaultNetworkUsage(MIGRATION_OPTION_ENABLED).build()
-
-        assertEquals(MIGRATION_OPTION_ENABLED, options.allowNonDefaultNetworkUsage)
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt b/Cronet/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt
deleted file mode 100644
index 56802c6..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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 android.net.http.cts
-
-import android.net.http.DnsOptions
-import android.net.http.DnsOptions.DNS_OPTION_ENABLED
-import android.net.http.DnsOptions.DNS_OPTION_UNSPECIFIED
-import android.os.Build
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import java.time.Duration
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-import kotlin.test.assertNull
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class DnsOptionsTest {
-
-    @Test
-    fun testDnsOptions_defaultValues() {
-        val options = DnsOptions.Builder().build()
-
-        assertEquals(DNS_OPTION_UNSPECIFIED, options.persistHostCache)
-        assertNull(options.persistHostCachePeriod)
-        assertEquals(DNS_OPTION_UNSPECIFIED, options.staleDns)
-        assertNull(options.staleDnsOptions)
-        assertEquals(DNS_OPTION_UNSPECIFIED, options.useHttpStackDnsResolver)
-        assertEquals(DNS_OPTION_UNSPECIFIED,
-                options.preestablishConnectionsToStaleDnsResults)
-    }
-
-    @Test
-    fun testDnsOptions_persistHostCache_returnSetValue() {
-        val options = DnsOptions.Builder()
-                .setPersistHostCache(DNS_OPTION_ENABLED)
-                .build()
-
-        assertEquals(DNS_OPTION_ENABLED, options.persistHostCache)
-    }
-
-    @Test
-    fun testDnsOptions_persistHostCachePeriod_returnSetValue() {
-        val period = Duration.ofMillis(12345)
-        val options = DnsOptions.Builder().setPersistHostCachePeriod(period).build()
-
-        assertEquals(period, options.persistHostCachePeriod)
-    }
-
-    @Test
-    fun testDnsOptions_enableStaleDns_returnSetValue() {
-        val options = DnsOptions.Builder()
-                .setStaleDns(DNS_OPTION_ENABLED)
-                .build()
-
-        assertEquals(DNS_OPTION_ENABLED, options.staleDns)
-    }
-
-    @Test
-    fun testDnsOptions_useHttpStackDnsResolver_returnsSetValue() {
-        val options = DnsOptions.Builder()
-                .setUseHttpStackDnsResolver(DNS_OPTION_ENABLED)
-                .build()
-
-        assertEquals(DNS_OPTION_ENABLED, options.useHttpStackDnsResolver)
-    }
-
-    @Test
-    fun testDnsOptions_preestablishConnectionsToStaleDnsResults_returnsSetValue() {
-        val options = DnsOptions.Builder()
-                .setPreestablishConnectionsToStaleDnsResults(DNS_OPTION_ENABLED)
-                .build()
-
-        assertEquals(DNS_OPTION_ENABLED,
-                options.preestablishConnectionsToStaleDnsResults)
-    }
-
-    @Test
-    fun testDnsOptions_setStaleDnsOptions_returnsSetValues() {
-        val staleOptions = DnsOptions.StaleDnsOptions.Builder()
-                .setAllowCrossNetworkUsage(DNS_OPTION_ENABLED)
-                .setFreshLookupTimeout(Duration.ofMillis(1234))
-                .build()
-        val options = DnsOptions.Builder()
-                .setStaleDns(DNS_OPTION_ENABLED)
-                .setStaleDnsOptions(staleOptions)
-                .build()
-
-        assertEquals(DNS_OPTION_ENABLED, options.staleDns)
-        assertEquals(staleOptions, options.staleDnsOptions)
-    }
-
-    @Test
-    fun testStaleDnsOptions_defaultValues() {
-        val options = DnsOptions.StaleDnsOptions.Builder().build()
-
-        assertEquals(DNS_OPTION_UNSPECIFIED, options.allowCrossNetworkUsage)
-        assertNull(options.freshLookupTimeout)
-        assertNull(options.maxExpiredDelay)
-        assertEquals(DNS_OPTION_UNSPECIFIED, options.useStaleOnNameNotResolved)
-    }
-
-    @Test
-    fun testStaleDnsOptions_allowCrossNetworkUsage_returnsSetValue() {
-        val options = DnsOptions.StaleDnsOptions.Builder()
-                .setAllowCrossNetworkUsage(DNS_OPTION_ENABLED).build()
-
-        assertEquals(DNS_OPTION_ENABLED, options.allowCrossNetworkUsage)
-    }
-
-    @Test
-    fun testStaleDnsOptions_freshLookupTimeout_returnsSetValue() {
-        val duration = Duration.ofMillis(12345)
-        val options = DnsOptions.StaleDnsOptions.Builder().setFreshLookupTimeout(duration).build()
-
-        assertNotNull(options.freshLookupTimeout)
-        assertEquals(duration, options.freshLookupTimeout!!)
-    }
-
-    @Test
-    fun testStaleDnsOptions_useStaleOnNameNotResolved_returnsSetValue() {
-        val options = DnsOptions.StaleDnsOptions.Builder()
-                .setUseStaleOnNameNotResolved(DNS_OPTION_ENABLED)
-                .build()
-
-        assertEquals(DNS_OPTION_ENABLED, options.useStaleOnNameNotResolved)
-    }
-
-    @Test
-    fun testStaleDnsOptions_maxExpiredDelayMillis_returnsSetValue() {
-        val duration = Duration.ofMillis(12345)
-        val options = DnsOptions.StaleDnsOptions.Builder().setMaxExpiredDelay(duration).build()
-
-        assertNotNull(options.maxExpiredDelay)
-        assertEquals(duration, options.maxExpiredDelay!!)
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
deleted file mode 100644
index f86ac29..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
+++ /dev/null
@@ -1,402 +0,0 @@
-/*
- * 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 android.net.http.cts;
-
-import static android.net.http.cts.util.TestUtilsKt.assertOKStatusCode;
-import static android.net.http.cts.util.TestUtilsKt.assumeOKStatusCode;
-import static android.net.http.cts.util.TestUtilsKt.skipIfNoInternetConnection;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.net.Network;
-import android.net.http.ConnectionMigrationOptions;
-import android.net.http.DnsOptions;
-import android.net.http.HttpEngine;
-import android.net.http.QuicOptions;
-import android.net.http.UrlRequest;
-import android.net.http.UrlResponseInfo;
-import android.net.http.cts.util.HttpCtsTestServer;
-import android.net.http.cts.util.TestUrlRequestCallback;
-import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
-import android.os.Build;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Set;
-
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class HttpEngineTest {
-    private static final String HOST = "source.android.com";
-    private static final String URL = "https://" + HOST;
-
-    private HttpEngine.Builder mEngineBuilder;
-    private TestUrlRequestCallback mCallback;
-    private HttpCtsTestServer mTestServer;
-    private UrlRequest mRequest;
-    private HttpEngine mEngine;
-    private Context mContext;
-
-    @Before
-    public void setUp() throws Exception {
-        mContext = ApplicationProvider.getApplicationContext();
-        skipIfNoInternetConnection(mContext);
-        mEngineBuilder = new HttpEngine.Builder(mContext);
-        mCallback = new TestUrlRequestCallback();
-        mTestServer = new HttpCtsTestServer(mContext);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (mRequest != null) {
-            mRequest.cancel();
-            mCallback.blockForDone();
-        }
-        if (mEngine != null) {
-            mEngine.shutdown();
-        }
-        if (mTestServer != null) {
-            mTestServer.shutdown();
-        }
-    }
-
-    private boolean isQuic(String negotiatedProtocol) {
-        return negotiatedProtocol.startsWith("http/2+quic") || negotiatedProtocol.startsWith("h3");
-    }
-
-    @Test
-    public void testHttpEngine_Default() throws Exception {
-        mEngine = mEngineBuilder.build();
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-
-        // This tests uses a non-hermetic server. Instead of asserting, assume the next callback.
-        // This way, if the request were to fail, the test would just be skipped instead of failing.
-        mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-        assertEquals("h2", info.getNegotiatedProtocol());
-    }
-
-    @Test
-    public void testHttpEngine_EnableHttpCache() {
-        String url = mTestServer.getCacheableTestDownloadUrl(
-                /* downloadId */ "cacheable-download",
-                /* numBytes */ 10);
-        mEngine =
-                mEngineBuilder
-                        .setStoragePath(mContext.getApplicationInfo().dataDir)
-                        .setEnableHttpCache(
-                                HttpEngine.Builder.HTTP_CACHE_DISK, /* maxSize */ 100 * 1024)
-                        .build();
-
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assumeOKStatusCode(info);
-        assertFalse(info.wasCached());
-
-        mCallback = new TestUrlRequestCallback();
-        builder = mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-        assertTrue(info.wasCached());
-    }
-
-    @Test
-    public void testHttpEngine_DisableHttp2() throws Exception {
-        mEngine = mEngineBuilder.setEnableHttp2(false).build();
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-
-        // This tests uses a non-hermetic server. Instead of asserting, assume the next callback.
-        // This way, if the request were to fail, the test would just be skipped instead of failing.
-        mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-        assertEquals("http/1.1", info.getNegotiatedProtocol());
-    }
-
-    @Test
-    public void testHttpEngine_EnablePublicKeyPinningBypassForLocalTrustAnchors() {
-        String url = mTestServer.getSuccessUrl();
-        // For known hosts, requests should succeed whether we're bypassing the local trust anchor
-        // or not.
-        mEngine = mEngineBuilder.setEnablePublicKeyPinningBypassForLocalTrustAnchors(false).build();
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-
-        mEngine.shutdown();
-        mEngine = mEngineBuilder.setEnablePublicKeyPinningBypassForLocalTrustAnchors(true).build();
-        mCallback = new TestUrlRequestCallback();
-        builder = mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-
-        // TODO(b/270918920): We should also test with a certificate not present in the device's
-        // trusted store.
-        // This requires either:
-        // * Mocking the underlying CertificateVerifier.
-        // * Or, having the server return a root certificate not present in the device's trusted
-        //   store.
-        // The former doesn't make sense for a CTS test as it would depend on the underlying
-        // implementation. The latter is something we should support once we write a proper test
-        // server.
-    }
-
-    private byte[] generateSha256() {
-        byte[] sha256 = new byte[32];
-        Arrays.fill(sha256, (byte) 58);
-        return sha256;
-    }
-
-    private Instant instantInFuture(int secondsIntoFuture) {
-        Calendar cal = Calendar.getInstance();
-        cal.add(Calendar.SECOND, secondsIntoFuture);
-        return cal.getTime().toInstant();
-    }
-
-    @Test
-    public void testHttpEngine_AddPublicKeyPins() {
-        // CtsTestServer, when set in SslMode.NO_CLIENT_AUTH (required to trigger
-        // certificate verification, needed by this test), uses a certificate that
-        // doesn't match the hostname. For this reason, CtsTestServer cannot be used
-        // by this test.
-        Instant expirationInstant = instantInFuture(/* secondsIntoFuture */ 100);
-        boolean includeSubdomains = true;
-        Set<byte[]> pinsSha256 = Set.of(generateSha256());
-        mEngine = mEngineBuilder.addPublicKeyPins(
-                HOST, pinsSha256, includeSubdomains, expirationInstant).build();
-
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-        mCallback.expectCallback(ResponseStep.ON_FAILED);
-        assertNotNull("Expected an error", mCallback.mError);
-    }
-
-    @Test
-    public void testHttpEngine_EnableQuic() throws Exception {
-        String url = mTestServer.getSuccessUrl();
-        mEngine = mEngineBuilder.setEnableQuic(true).addQuicHint(HOST, 443, 443).build();
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-    }
-
-    @Test
-    public void testHttpEngine_GetDefaultUserAgent() throws Exception {
-        assertThat(mEngineBuilder.getDefaultUserAgent(), containsString("AndroidHttpClient"));
-        assertThat(mEngineBuilder.getDefaultUserAgent()).contains(HttpEngine.getVersionString());
-    }
-
-    @Test
-    public void testHttpEngine_requestUsesDefaultUserAgent() throws Exception {
-        mEngine = mEngineBuilder.build();
-
-        String url = mTestServer.getUserAgentUrl();
-        UrlRequest request =
-                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build();
-        request.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-        String receivedUserAgent = extractUserAgent(mCallback.mResponseAsString);
-
-        assertThat(receivedUserAgent).isEqualTo(mEngineBuilder.getDefaultUserAgent());
-    }
-
-    @Test
-    public void testHttpEngine_requestUsesCustomUserAgent() throws Exception {
-        String userAgent = "CtsTests User Agent";
-        mEngine =
-                new HttpEngine.Builder(ApplicationProvider.getApplicationContext())
-                        .setUserAgent(userAgent)
-                        .build();
-
-        String url = mTestServer.getUserAgentUrl();
-        UrlRequest request =
-                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build();
-        request.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-        String receivedUserAgent = extractUserAgent(mCallback.mResponseAsString);
-
-        assertThat(receivedUserAgent).isEqualTo(userAgent);
-    }
-
-    private static String extractUserAgent(String userAgentResponseBody) {
-        // If someone wants to be evil and have the title HTML tag a part of the user agent,
-        // they'll have to fix this method :)
-        return userAgentResponseBody.replaceFirst(".*<title>", "").replaceFirst("</title>.*", "");
-    }
-
-    @Test
-    public void testHttpEngine_bindToNetwork() throws Exception {
-        // Create a fake Android.net.Network. Since that network doesn't exist, binding to
-        // that should end up in a failed request.
-        Network mockNetwork = Mockito.mock(Network.class);
-        Mockito.when(mockNetwork.getNetworkHandle()).thenReturn(123L);
-        String url = mTestServer.getSuccessUrl();
-
-        mEngine = mEngineBuilder.build();
-        mEngine.bindToNetwork(mockNetwork);
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-
-        mCallback.expectCallback(ResponseStep.ON_FAILED);
-    }
-
-    @Test
-    public void testHttpEngine_unbindFromNetwork() throws Exception {
-        // Create a fake Android.net.Network. Since that network doesn't exist, binding to
-        // that should end up in a failed request.
-        Network mockNetwork = Mockito.mock(Network.class);
-        Mockito.when(mockNetwork.getNetworkHandle()).thenReturn(123L);
-        String url = mTestServer.getSuccessUrl();
-
-        mEngine = mEngineBuilder.build();
-        // Bind to the fake network but then unbind. This should result in a successful
-        // request.
-        mEngine.bindToNetwork(mockNetwork);
-        mEngine.bindToNetwork(null);
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-    }
-
-    @Test
-    public void testHttpEngine_setConnectionMigrationOptions_requestSucceeds() {
-        ConnectionMigrationOptions options = new ConnectionMigrationOptions.Builder().build();
-        mEngine = mEngineBuilder.setConnectionMigrationOptions(options).build();
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(
-                        mTestServer.getSuccessUrl(), mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-    }
-
-    @Test
-    public void testHttpEngine_setDnsOptions_requestSucceeds() {
-        DnsOptions options = new DnsOptions.Builder().build();
-        mEngine = mEngineBuilder.setDnsOptions(options).build();
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(
-                        mTestServer.getSuccessUrl(), mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-    }
-
-    @Test
-    public void getVersionString_notEmpty() {
-        assertThat(HttpEngine.getVersionString()).isNotEmpty();
-    }
-
-    @Test
-    public void testHttpEngine_SetQuicOptions_RequestSucceedsWithQuic() throws Exception {
-        String url = mTestServer.getSuccessUrl();
-        QuicOptions options = new QuicOptions.Builder().build();
-        mEngine = mEngineBuilder
-                .setEnableQuic(true)
-                .addQuicHint(HOST, 443, 443)
-                .setQuicOptions(options)
-                .build();
-        UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-        mRequest = builder.build();
-        mRequest.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-
-    }
-
-    @Test
-    public void testHttpEngine_enableBrotli_brotliAdvertised() {
-        mEngine = mEngineBuilder.setEnableBrotli(true).build();
-        mRequest =
-                mEngine.newUrlRequestBuilder(
-                        mTestServer.getEchoHeadersUrl(), mCallback.getExecutor(), mCallback)
-                        .build();
-        mRequest.start();
-
-        mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertThat(info.getHeaders().getAsMap().get("x-request-header-Accept-Encoding").toString())
-                .contains("br");
-        assertOKStatusCode(info);
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt b/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt
deleted file mode 100644
index cff54b3..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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 android.net.http.cts
-
-import android.net.http.HttpEngine
-import android.net.http.NetworkException
-import android.net.http.cts.util.TestUrlRequestCallback
-import android.os.Build
-import androidx.test.core.app.ApplicationProvider
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import kotlin.test.assertEquals
-import kotlin.test.assertIs
-import kotlin.test.assertSame
-import kotlin.test.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class NetworkExceptionTest {
-
-    @Test
-    fun testNetworkException_returnsInputParameters() {
-        val message = "failed"
-        val cause = Throwable("thrown")
-        val networkException =
-            object : NetworkException(message, cause) {
-                override fun getErrorCode() = 0
-                override fun isImmediatelyRetryable() = false
-            }
-
-        assertEquals(message, networkException.message)
-        assertSame(cause, networkException.cause)
-    }
-
-    @Test
-    fun testNetworkException_thrownFromUrlRequest() {
-        val httpEngine = HttpEngine.Builder(ApplicationProvider.getApplicationContext()).build()
-        val callback = TestUrlRequestCallback()
-        val request =
-            httpEngine.newUrlRequestBuilder("http://localhost", callback.executor, callback).build()
-
-        request.start()
-        callback.blockForDone()
-
-        assertTrue(request.isDone)
-        assertIs<NetworkException>(callback.mError)
-        httpEngine.shutdown()
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt b/Cronet/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt
deleted file mode 100644
index 2705fc3..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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 android.net.http.cts
-
-import android.net.http.QuicException
-import android.os.Build
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class QuicExceptionTest {
-
-    @Test
-    fun testQuicException_returnsInputParameters() {
-        val message = "failed"
-        val cause = Throwable("thrown")
-        val quicException =
-            object : QuicException(message, cause) {
-                override fun getErrorCode() = 0
-                override fun isImmediatelyRetryable() = false
-            }
-
-        assertEquals(message, quicException.message)
-        assertEquals(cause, quicException.cause)
-    }
-
-    // TODO: add test for QuicException triggered from HttpEngine
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt b/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt
deleted file mode 100644
index da0b15c..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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 android.net.http.cts
-
-import android.net.http.QuicOptions
-import android.os.Build
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import com.google.common.truth.Truth.assertThat
-import java.time.Duration
-import kotlin.test.assertFailsWith
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class QuicOptionsTest {
-    @Test
-    fun testQuicOptions_defaultValues() {
-        val quicOptions = QuicOptions.Builder().build()
-        assertThat(quicOptions.allowedQuicHosts).isEmpty()
-        assertThat(quicOptions.handshakeUserAgent).isNull()
-        assertThat(quicOptions.idleConnectionTimeout).isNull()
-        assertFalse(quicOptions.hasInMemoryServerConfigsCacheSize())
-        assertFailsWith(IllegalStateException::class) {
-            quicOptions.inMemoryServerConfigsCacheSize
-        }
-    }
-
-    @Test
-    fun testQuicOptions_quicHostAllowlist_returnsAddedValues() {
-        val quicOptions = QuicOptions.Builder()
-                .addAllowedQuicHost("foo")
-                .addAllowedQuicHost("bar")
-                .addAllowedQuicHost("foo")
-                .addAllowedQuicHost("baz")
-                .build()
-        assertThat(quicOptions.allowedQuicHosts)
-                .containsExactly("foo", "bar", "baz")
-                .inOrder()
-    }
-
-    @Test
-    fun testQuicOptions_idleConnectionTimeout_returnsSetValue() {
-        val timeout = Duration.ofMinutes(10)
-        val quicOptions = QuicOptions.Builder()
-                .setIdleConnectionTimeout(timeout)
-                .build()
-        assertThat(quicOptions.idleConnectionTimeout)
-                .isEqualTo(timeout)
-    }
-
-    @Test
-    fun testQuicOptions_inMemoryServerConfigsCacheSize_returnsSetValue() {
-        val quicOptions = QuicOptions.Builder()
-                .setInMemoryServerConfigsCacheSize(42)
-                .build()
-        assertTrue(quicOptions.hasInMemoryServerConfigsCacheSize())
-        assertThat(quicOptions.inMemoryServerConfigsCacheSize)
-                .isEqualTo(42)
-    }
-
-    @Test
-    fun testQuicOptions_handshakeUserAgent_returnsSetValue() {
-        val userAgent = "test"
-        val quicOptions = QuicOptions.Builder()
-            .setHandshakeUserAgent(userAgent)
-            .build()
-        assertThat(quicOptions.handshakeUserAgent)
-            .isEqualTo(userAgent)
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
deleted file mode 100644
index 3c4d134..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
- * 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 android.net.http.cts;
-
-import static android.net.http.cts.util.TestUtilsKt.assertOKStatusCode;
-import static android.net.http.cts.util.TestUtilsKt.skipIfNoInternetConnection;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.greaterThan;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import android.content.Context;
-import android.net.http.HeaderBlock;
-import android.net.http.HttpEngine;
-import android.net.http.HttpException;
-import android.net.http.InlineExecutionProhibitedException;
-import android.net.http.UploadDataProvider;
-import android.net.http.UrlRequest;
-import android.net.http.UrlRequest.Status;
-import android.net.http.UrlResponseInfo;
-import android.net.http.cts.util.HttpCtsTestServer;
-import android.net.http.cts.util.TestStatusListener;
-import android.net.http.cts.util.TestUrlRequestCallback;
-import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
-import android.net.http.cts.util.UploadDataProviders;
-import android.os.Build;
-import android.webkit.cts.CtsTestServer;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import com.google.common.base.Strings;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.net.URLEncoder;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class UrlRequestTest {
-    private static final Executor DIRECT_EXECUTOR = Runnable::run;
-
-    private TestUrlRequestCallback mCallback;
-    private HttpCtsTestServer mTestServer;
-    private HttpEngine mHttpEngine;
-
-    @Before
-    public void setUp() throws Exception {
-        Context context = ApplicationProvider.getApplicationContext();
-        skipIfNoInternetConnection(context);
-        HttpEngine.Builder builder = new HttpEngine.Builder(context);
-        mHttpEngine = builder.build();
-        mCallback = new TestUrlRequestCallback();
-        mTestServer = new HttpCtsTestServer(context);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (mHttpEngine != null) {
-            mHttpEngine.shutdown();
-        }
-        if (mTestServer != null) {
-            mTestServer.shutdown();
-        }
-    }
-
-    private UrlRequest.Builder createUrlRequestBuilder(String url) {
-        return mHttpEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
-    }
-
-    @Test
-    public void testUrlRequestGet_CompletesSuccessfully() throws Exception {
-        String url = mTestServer.getSuccessUrl();
-        UrlRequest request = createUrlRequestBuilder(url).build();
-        request.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-        assertThat("Received byte count must be > 0", info.getReceivedByteCount(), greaterThan(0L));
-    }
-
-    @Test
-    public void testUrlRequestStatus_InvalidBeforeRequestStarts() throws Exception {
-        UrlRequest request = createUrlRequestBuilder(mTestServer.getSuccessUrl()).build();
-        // Calling before request is started should give Status.INVALID,
-        // since the native adapter is not created.
-        TestStatusListener statusListener = new TestStatusListener();
-        request.getStatus(statusListener);
-        statusListener.expectStatus(Status.INVALID);
-    }
-
-    @Test
-    public void testUrlRequestCancel_CancelCalled() throws Exception {
-        UrlRequest request = createUrlRequestBuilder(mTestServer.getSuccessUrl()).build();
-        mCallback.setAutoAdvance(false);
-
-        request.start();
-        mCallback.waitForNextStep();
-        assertSame(mCallback.mResponseStep, ResponseStep.ON_RESPONSE_STARTED);
-
-        request.cancel();
-        mCallback.expectCallback(ResponseStep.ON_CANCELED);
-    }
-
-    @Test
-    public void testUrlRequestPost_EchoRequestBody() {
-        String testData = "test";
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getEchoBodyUrl());
-
-        UploadDataProvider dataProvider = UploadDataProviders.create(testData);
-        builder.setUploadDataProvider(dataProvider, mCallback.getExecutor());
-        builder.addHeader("Content-Type", "text/html");
-        builder.build().start();
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-
-        assertOKStatusCode(mCallback.mResponseInfo);
-        assertEquals(testData, mCallback.mResponseAsString);
-    }
-
-    @Test
-    public void testUrlRequestFail_FailedCalled() {
-        createUrlRequestBuilder("http://0.0.0.0:0/").build().start();
-        mCallback.expectCallback(ResponseStep.ON_FAILED);
-    }
-
-    @Test
-    public void testUrlRequest_directExecutor_allowed() throws InterruptedException {
-        TestUrlRequestCallback callback = new TestUrlRequestCallback();
-        callback.setAllowDirectExecutor(true);
-        UrlRequest.Builder builder = mHttpEngine.newUrlRequestBuilder(
-                mTestServer.getEchoBodyUrl(), DIRECT_EXECUTOR, callback);
-        UploadDataProvider dataProvider = UploadDataProviders.create("test");
-        builder.setUploadDataProvider(dataProvider, DIRECT_EXECUTOR);
-        builder.addHeader("Content-Type", "text/plain;charset=UTF-8");
-        builder.setDirectExecutorAllowed(true);
-        builder.build().start();
-        callback.blockForDone();
-
-        if (callback.mOnErrorCalled) {
-            throw new AssertionError("Expected no exception", callback.mError);
-        }
-
-        assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
-        assertEquals("test", callback.mResponseAsString);
-    }
-
-    @Test
-    public void testUrlRequest_directExecutor_disallowed_uploadDataProvider() throws Exception {
-        TestUrlRequestCallback callback = new TestUrlRequestCallback();
-        // This applies just locally to the test callback, not to SUT
-        callback.setAllowDirectExecutor(true);
-
-        UrlRequest.Builder builder = mHttpEngine.newUrlRequestBuilder(
-                mTestServer.getEchoBodyUrl(), Executors.newSingleThreadExecutor(), callback);
-        UploadDataProvider dataProvider = UploadDataProviders.create("test");
-
-        builder.setUploadDataProvider(dataProvider, DIRECT_EXECUTOR)
-                .addHeader("Content-Type", "text/plain;charset=UTF-8")
-                .build()
-                .start();
-        callback.blockForDone();
-
-        assertTrue(callback.mOnErrorCalled);
-        assertTrue(callback.mError.getCause() instanceof InlineExecutionProhibitedException);
-    }
-
-    @Test
-    public void testUrlRequest_directExecutor_disallowed_responseCallback() throws Exception {
-        TestUrlRequestCallback callback = new TestUrlRequestCallback();
-        // This applies just locally to the test callback, not to SUT
-        callback.setAllowDirectExecutor(true);
-
-        UrlRequest.Builder builder = mHttpEngine.newUrlRequestBuilder(
-                mTestServer.getEchoBodyUrl(), DIRECT_EXECUTOR, callback);
-        UploadDataProvider dataProvider = UploadDataProviders.create("test");
-
-        builder.setUploadDataProvider(dataProvider, Executors.newSingleThreadExecutor())
-                .addHeader("Content-Type", "text/plain;charset=UTF-8")
-                .build()
-                .start();
-        callback.blockForDone();
-
-        assertTrue(callback.mOnErrorCalled);
-        assertTrue(callback.mError.getCause() instanceof InlineExecutionProhibitedException);
-    }
-
-    @Test
-    public void testUrlRequest_nonDirectByteBuffer() throws Exception {
-        BlockingQueue<HttpException> onFailedException = new ArrayBlockingQueue<>(1);
-
-        UrlRequest request =
-                mHttpEngine
-                        .newUrlRequestBuilder(
-                                mTestServer.getSuccessUrl(),
-                                Executors.newSingleThreadExecutor(),
-                                new StubUrlRequestCallback() {
-                                    @Override
-                                    public void onResponseStarted(
-                                            UrlRequest request, UrlResponseInfo info) {
-                                        // note: allocate, not allocateDirect
-                                        request.read(ByteBuffer.allocate(1024));
-                                    }
-
-                                    @Override
-                                    public void onFailed(
-                                            UrlRequest request,
-                                            UrlResponseInfo info,
-                                            HttpException error) {
-                                        onFailedException.add(error);
-                                    }
-                                })
-                        .build();
-        request.start();
-
-        HttpException e = onFailedException.poll(5, TimeUnit.SECONDS);
-        assertNotNull(e);
-        assertTrue(e.getCause() instanceof IllegalArgumentException);
-        assertTrue(e.getCause().getMessage().contains("direct"));
-    }
-
-    @Test
-    public void testUrlRequest_fullByteBuffer() throws Exception {
-        BlockingQueue<HttpException> onFailedException = new ArrayBlockingQueue<>(1);
-
-        UrlRequest request =
-                mHttpEngine
-                        .newUrlRequestBuilder(
-                                mTestServer.getSuccessUrl(),
-                                Executors.newSingleThreadExecutor(),
-                                new StubUrlRequestCallback() {
-                                    @Override
-                                    public void onResponseStarted(
-                                            UrlRequest request, UrlResponseInfo info) {
-                                        ByteBuffer bb = ByteBuffer.allocateDirect(1024);
-                                        bb.position(bb.limit());
-                                        request.read(bb);
-                                    }
-
-                                    @Override
-                                    public void onFailed(
-                                            UrlRequest request,
-                                            UrlResponseInfo info,
-                                            HttpException error) {
-                                        onFailedException.add(error);
-                                    }
-                                })
-                        .build();
-        request.start();
-
-        HttpException e = onFailedException.poll(5, TimeUnit.SECONDS);
-        assertNotNull(e);
-        assertTrue(e.getCause() instanceof IllegalArgumentException);
-        assertTrue(e.getCause().getMessage().contains("full"));
-    }
-
-    @Test
-    public void testUrlRequest_redirects() throws Exception {
-        int expectedNumRedirects = 5;
-        String url =
-                mTestServer.getRedirectingAssetUrl("html/hello_world.html", expectedNumRedirects);
-
-        UrlRequest request = createUrlRequestBuilder(url).build();
-        request.start();
-
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-        UrlResponseInfo info = mCallback.mResponseInfo;
-        assertOKStatusCode(info);
-        assertThat(mCallback.mResponseAsString).contains("hello world");
-        assertThat(info.getUrlChain()).hasSize(expectedNumRedirects + 1);
-        assertThat(info.getUrlChain().get(0)).isEqualTo(url);
-        assertThat(info.getUrlChain().get(expectedNumRedirects)).isEqualTo(info.getUrl());
-    }
-
-    @Test
-    public void testUrlRequestPost_withRedirect() throws Exception {
-        String body = Strings.repeat(
-                "Hello, this is a really interesting body, so write this 100 times.", 100);
-
-        String redirectUrlParameter =
-                URLEncoder.encode(mTestServer.getEchoBodyUrl(), "UTF-8");
-        createUrlRequestBuilder(
-                String.format(
-                        "%s/alt_redirect?dest=%s&statusCode=307",
-                        mTestServer.getBaseUri(),
-                        redirectUrlParameter))
-                .setHttpMethod("POST")
-                .addHeader("Content-Type", "text/plain")
-                .setUploadDataProvider(
-                        UploadDataProviders.create(body.getBytes(StandardCharsets.UTF_8)),
-                        mCallback.getExecutor())
-                .build()
-                .start();
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-
-        assertOKStatusCode(mCallback.mResponseInfo);
-        assertThat(mCallback.mResponseAsString).isEqualTo(body);
-    }
-
-    @Test
-    public void testUrlRequest_customHeaders() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getEchoHeadersUrl());
-
-        List<Map.Entry<String, String>> expectedHeaders = Arrays.asList(
-                Map.entry("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="),
-                Map.entry("Max-Forwards", "10"),
-                Map.entry("X-Client-Data", "random custom header content"));
-
-        for (Map.Entry<String, String> header : expectedHeaders) {
-            builder.addHeader(header.getKey(), header.getValue());
-        }
-
-        builder.build().start();
-        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-
-        assertOKStatusCode(mCallback.mResponseInfo);
-
-        List<Map.Entry<String, String>> echoedHeaders =
-                extractEchoedHeaders(mCallback.mResponseInfo.getHeaders());
-
-        // The implementation might decide to add more headers like accepted encodings it handles
-        // internally so the server is likely to see more headers than explicitly set
-        // by the developer.
-        assertThat(echoedHeaders)
-                .containsAtLeastElementsIn(expectedHeaders);
-    }
-
-    @Test
-    public void testUrlRequest_getHttpMethod() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-        final String method = "POST";
-
-        builder.setHttpMethod(method);
-        UrlRequest request = builder.build();
-        assertThat(request.getHttpMethod()).isEqualTo(method);
-    }
-
-    @Test
-    public void testUrlRequest_getHeaders_asList() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-        final List<Map.Entry<String, String>> expectedHeaders = Arrays.asList(
-                Map.entry("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="),
-                Map.entry("Max-Forwards", "10"),
-                Map.entry("X-Client-Data", "random custom header content"));
-
-        for (Map.Entry<String, String> header : expectedHeaders) {
-            builder.addHeader(header.getKey(), header.getValue());
-        }
-
-        UrlRequest request = builder.build();
-        assertThat(request.getHeaders().getAsList()).containsAtLeastElementsIn(expectedHeaders);
-    }
-
-    @Test
-    public void testUrlRequest_getHeaders_asMap() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-        final Map<String, List<String>> expectedHeaders = Map.of(
-                "Authorization", Arrays.asList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="),
-                "Max-Forwards", Arrays.asList("10"),
-                "X-Client-Data", Arrays.asList("random custom header content"));
-
-        for (Map.Entry<String, List<String>> header : expectedHeaders.entrySet()) {
-            builder.addHeader(header.getKey(), header.getValue().get(0));
-        }
-
-        UrlRequest request = builder.build();
-        assertThat(request.getHeaders().getAsMap()).containsAtLeastEntriesIn(expectedHeaders);
-    }
-
-    @Test
-    public void testUrlRequest_isCacheDisabled() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-        final boolean isCacheDisabled = true;
-
-        builder.setCacheDisabled(isCacheDisabled);
-        UrlRequest request = builder.build();
-        assertThat(request.isCacheDisabled()).isEqualTo(isCacheDisabled);
-    }
-
-    @Test
-    public void testUrlRequest_isDirectExecutorAllowed() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-        final boolean isDirectExecutorAllowed = true;
-
-        builder.setDirectExecutorAllowed(isDirectExecutorAllowed);
-        UrlRequest request = builder.build();
-        assertThat(request.isDirectExecutorAllowed()).isEqualTo(isDirectExecutorAllowed);
-    }
-
-    @Test
-    public void testUrlRequest_getPriority() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-        final int priority = UrlRequest.REQUEST_PRIORITY_LOW;
-
-        builder.setPriority(priority);
-        UrlRequest request = builder.build();
-        assertThat(request.getPriority()).isEqualTo(priority);
-    }
-
-    @Test
-    public void testUrlRequest_hasTrafficStatsTag() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-
-        builder.setTrafficStatsTag(10);
-        UrlRequest request = builder.build();
-        assertThat(request.hasTrafficStatsTag()).isEqualTo(true);
-    }
-
-    @Test
-    public void testUrlRequest_getTrafficStatsTag() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-        final int trafficStatsTag = 10;
-
-        builder.setTrafficStatsTag(trafficStatsTag);
-        UrlRequest request = builder.build();
-        assertThat(request.getTrafficStatsTag()).isEqualTo(trafficStatsTag);
-    }
-
-    @Test
-    public void testUrlRequest_hasTrafficStatsUid() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-
-        builder.setTrafficStatsUid(10);
-        UrlRequest request = builder.build();
-        assertThat(request.hasTrafficStatsUid()).isEqualTo(true);
-    }
-
-    @Test
-    public void testUrlRequest_getTrafficStatsUid() throws Exception {
-        UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl());
-        final int trafficStatsUid = 10;
-
-        builder.setTrafficStatsUid(trafficStatsUid);
-        UrlRequest request = builder.build();
-        assertThat(request.getTrafficStatsUid()).isEqualTo(trafficStatsUid);
-    }
-
-    private static List<Map.Entry<String, String>> extractEchoedHeaders(HeaderBlock headers) {
-        return headers.getAsList()
-                .stream()
-                .flatMap(input -> {
-                    if (input.getKey().startsWith(CtsTestServer.ECHOED_RESPONSE_HEADER_PREFIX)) {
-                        String strippedKey =
-                                input.getKey().substring(
-                                        CtsTestServer.ECHOED_RESPONSE_HEADER_PREFIX.length());
-                        return Stream.of(Map.entry(strippedKey, input.getValue()));
-                    } else {
-                        return Stream.empty();
-                    }
-                })
-                .collect(Collectors.toList());
-    }
-
-    private static class StubUrlRequestCallback implements UrlRequest.Callback {
-
-        @Override
-        public void onRedirectReceived(
-                UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onReadCompleted(
-                UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onFailed(UrlRequest request, UrlResponseInfo info, HttpException error) {
-            throw new UnsupportedOperationException(error);
-        }
-
-        @Override
-        public void onCanceled(@NonNull UrlRequest request, @Nullable UrlResponseInfo info) {
-            throw new UnsupportedOperationException();
-        }
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt b/Cronet/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt
deleted file mode 100644
index f1b57c6..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 android.net.http.cts
-
-import android.content.Context
-import android.net.http.HttpEngine
-import android.net.http.cts.util.HttpCtsTestServer
-import android.net.http.cts.util.TestUrlRequestCallback
-import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep
-import android.os.Build
-import androidx.test.core.app.ApplicationProvider
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class UrlResponseInfoTest {
-
-    @Test
-    fun testUrlResponseInfo_apisReturnCorrectInfo() {
-        // start the engine and send a request
-        val context: Context = ApplicationProvider.getApplicationContext()
-        val server = HttpCtsTestServer(context)
-        val httpEngine = HttpEngine.Builder(context).build()
-        val callback = TestUrlRequestCallback()
-        val url = server.successUrl
-        val request = httpEngine.newUrlRequestBuilder(url, callback.executor, callback).build()
-
-        request.start()
-        callback.expectCallback(ResponseStep.ON_SUCCEEDED)
-
-        val info = callback.mResponseInfo
-        assertFalse(info.headers.asList.isEmpty())
-        assertEquals(200, info.httpStatusCode)
-        assertTrue(info.receivedByteCount > 0)
-        assertEquals(url, info.url)
-        assertEquals(listOf(url), info.urlChain)
-        assertFalse(info.wasCached())
-
-        // TODO Current test server does not set these values. Uncomment when we use one that does.
-        // assertEquals("OK", info.httpStatusText)
-        // assertEquals("http/1.1", info.negotiatedProtocol)
-
-        // cronet defaults to port 0 when no proxy is specified.
-        // This is not a behaviour we want to enforce since null is reasonable too.
-        // assertEquals(":0", info.proxyServer)
-
-        server.shutdown()
-        httpEngine.shutdown()
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt b/Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt
deleted file mode 100644
index 5196544..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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 android.net.http.cts.util
-
-import android.content.Context
-import android.webkit.cts.CtsTestServer
-import java.net.URI
-import org.apache.http.HttpEntityEnclosingRequest
-import org.apache.http.HttpRequest
-import org.apache.http.HttpResponse
-import org.apache.http.HttpStatus
-import org.apache.http.HttpVersion
-import org.apache.http.message.BasicHttpResponse
-
-private const val ECHO_BODY_PATH = "/echo_body"
-
-/** Extends CtsTestServer to handle POST requests and other test specific requests */
-class HttpCtsTestServer(context: Context) : CtsTestServer(context) {
-
-    val echoBodyUrl: String = baseUri + ECHO_BODY_PATH
-    val successUrl: String = getAssetUrl("html/hello_world.html")
-
-    override fun onPost(req: HttpRequest): HttpResponse? {
-        val path = URI.create(req.requestLine.uri).path
-        var response: HttpResponse? = null
-
-        if (path.startsWith(ECHO_BODY_PATH)) {
-            if (req !is HttpEntityEnclosingRequest) {
-                return BasicHttpResponse(
-                    HttpVersion.HTTP_1_0,
-                    HttpStatus.SC_INTERNAL_SERVER_ERROR,
-                    "Expected req to be of type HttpEntityEnclosingRequest but got ${req.javaClass}"
-                )
-            }
-
-            response = BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_OK, null)
-            response.entity = req.entity
-            response.addHeader("Content-Length", req.entity.contentLength.toString())
-        }
-
-        return response
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestBidirectionalStreamCallback.java b/Cronet/tests/cts/src/android/net/http/cts/util/TestBidirectionalStreamCallback.java
deleted file mode 100644
index 1e7333c..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/util/TestBidirectionalStreamCallback.java
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
- * 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 android.net.http.cts.util;
-
-import static org.hamcrest.Matchers.equalTo;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeThat;
-import static org.junit.Assume.assumeTrue;
-
-import android.net.http.BidirectionalStream;
-import android.net.http.HeaderBlock;
-import android.net.http.HttpException;
-import android.net.http.UrlResponseInfo;
-import android.os.ConditionVariable;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-
-/**
- * Callback that tracks information from different callbacks and has a method to block thread until
- * the stream completes on another thread. Allows to cancel, block stream or throw an exception from
- * an arbitrary step.
- */
-public class TestBidirectionalStreamCallback implements BidirectionalStream.Callback {
-    private static final int TIMEOUT_MS = 12_000;
-    public UrlResponseInfo mResponseInfo;
-    public HttpException mError;
-
-    public ResponseStep mResponseStep = ResponseStep.NOTHING;
-
-    public boolean mOnErrorCalled;
-    public boolean mOnCanceledCalled;
-
-    public int mHttpResponseDataLength;
-    public String mResponseAsString = "";
-
-    public HeaderBlock mTrailers;
-
-    private static final int READ_BUFFER_SIZE = 32 * 1024;
-
-    // When false, the consumer is responsible for all calls into the stream
-    // that advance it.
-    private boolean mAutoAdvance = true;
-
-    // Conditionally fail on certain steps.
-    private FailureType mFailureType = FailureType.NONE;
-    private ResponseStep mFailureStep = ResponseStep.NOTHING;
-
-    // Signals when the stream is done either successfully or not.
-    private final ConditionVariable mDone = new ConditionVariable();
-
-    // Signaled on each step when mAutoAdvance is false.
-    private final ConditionVariable mReadStepBlock = new ConditionVariable();
-    private final ConditionVariable mWriteStepBlock = new ConditionVariable();
-
-    // Executor Service for Cronet callbacks.
-    private final ExecutorService mExecutorService =
-            Executors.newSingleThreadExecutor(new ExecutorThreadFactory());
-    private Thread mExecutorThread;
-
-    // position() of ByteBuffer prior to read() call.
-    private int mBufferPositionBeforeRead;
-
-    // Data to write.
-    private final ArrayList<WriteBuffer> mWriteBuffers = new ArrayList<WriteBuffer>();
-
-    // Buffers that we yet to receive the corresponding onWriteCompleted callback.
-    private final ArrayList<WriteBuffer> mWriteBuffersToBeAcked = new ArrayList<WriteBuffer>();
-
-    // Whether to use a direct executor.
-    private final boolean mUseDirectExecutor;
-    private final DirectExecutor mDirectExecutor;
-
-    private class ExecutorThreadFactory implements ThreadFactory {
-        @Override
-        public Thread newThread(Runnable r) {
-            mExecutorThread = new Thread(r);
-            return mExecutorThread;
-        }
-    }
-
-    private static class WriteBuffer {
-        final ByteBuffer mBuffer;
-        final boolean mFlush;
-
-        WriteBuffer(ByteBuffer buffer, boolean flush) {
-            mBuffer = buffer;
-            mFlush = flush;
-        }
-    }
-
-    private static class DirectExecutor implements Executor {
-        @Override
-        public void execute(Runnable task) {
-            task.run();
-        }
-    }
-
-    public enum ResponseStep {
-        NOTHING,
-        ON_STREAM_READY,
-        ON_RESPONSE_STARTED,
-        ON_READ_COMPLETED,
-        ON_WRITE_COMPLETED,
-        ON_TRAILERS,
-        ON_CANCELED,
-        ON_FAILED,
-        ON_SUCCEEDED,
-    }
-
-    public enum FailureType {
-        NONE,
-        CANCEL_SYNC,
-        CANCEL_ASYNC,
-        // Same as above, but continues to advance the stream after posting
-        // the cancellation task.
-        CANCEL_ASYNC_WITHOUT_PAUSE,
-        THROW_SYNC
-    }
-
-    private boolean isTerminalCallback(ResponseStep step) {
-        switch (step) {
-            case ON_SUCCEEDED:
-            case ON_CANCELED:
-            case ON_FAILED:
-                return true;
-            default:
-                return false;
-        }
-    }
-
-    public TestBidirectionalStreamCallback() {
-        mUseDirectExecutor = false;
-        mDirectExecutor = null;
-    }
-
-    public TestBidirectionalStreamCallback(boolean useDirectExecutor) {
-        mUseDirectExecutor = useDirectExecutor;
-        mDirectExecutor = new DirectExecutor();
-    }
-
-    public void setAutoAdvance(boolean autoAdvance) {
-        mAutoAdvance = autoAdvance;
-    }
-
-    public void setFailure(FailureType failureType, ResponseStep failureStep) {
-        mFailureStep = failureStep;
-        mFailureType = failureType;
-    }
-
-    public boolean blockForDone() {
-        return mDone.block(TIMEOUT_MS);
-    }
-
-    /**
-     * Waits for a terminal callback to complete execution before failing if the callback is not the
-     * expected one
-     *
-     * @param expectedStep the expected callback step
-     */
-    public void expectCallback(ResponseStep expectedStep) {
-        if (isTerminalCallback(expectedStep)) {
-            assertTrue(String.format(
-                            "Request timed out. Expected %s callback. Current callback is %s",
-                            expectedStep, mResponseStep),
-                    blockForDone());
-        }
-        assertSame(expectedStep, mResponseStep);
-    }
-
-    /**
-     * Waits for a terminal callback to complete execution before skipping the test if the callback
-     * is not the expected one
-     *
-     * @param expectedStep the expected callback step
-     */
-    public void assumeCallback(ResponseStep expectedStep) {
-        if (isTerminalCallback(expectedStep)) {
-            assumeTrue(
-                    String.format(
-                            "Request timed out. Expected %s callback. Current callback is %s",
-                            expectedStep, mResponseStep),
-                    blockForDone());
-        }
-        assumeThat(expectedStep, equalTo(mResponseStep));
-    }
-
-    public void waitForNextReadStep() {
-        mReadStepBlock.block();
-        mReadStepBlock.close();
-    }
-
-    public void waitForNextWriteStep() {
-        mWriteStepBlock.block();
-        mWriteStepBlock.close();
-    }
-
-    public Executor getExecutor() {
-        if (mUseDirectExecutor) {
-            return mDirectExecutor;
-        }
-        return mExecutorService;
-    }
-
-    public void shutdownExecutor() {
-        if (mUseDirectExecutor) {
-            throw new UnsupportedOperationException("DirectExecutor doesn't support shutdown");
-        }
-        mExecutorService.shutdown();
-    }
-
-    public void addWriteData(byte[] data) {
-        addWriteData(data, true);
-    }
-
-    public void addWriteData(byte[] data, boolean flush) {
-        ByteBuffer writeBuffer = ByteBuffer.allocateDirect(data.length);
-        writeBuffer.put(data);
-        writeBuffer.flip();
-        mWriteBuffers.add(new WriteBuffer(writeBuffer, flush));
-        mWriteBuffersToBeAcked.add(new WriteBuffer(writeBuffer, flush));
-    }
-
-    @Override
-    public void onStreamReady(BidirectionalStream stream) {
-        checkOnValidThread();
-        assertFalse(stream.isDone());
-        assertEquals(ResponseStep.NOTHING, mResponseStep);
-        assertNull(mError);
-        mResponseStep = ResponseStep.ON_STREAM_READY;
-        if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) {
-            return;
-        }
-        startNextWrite(stream);
-    }
-
-    @Override
-    public void onResponseHeadersReceived(BidirectionalStream stream, UrlResponseInfo info) {
-        checkOnValidThread();
-        assertFalse(stream.isDone());
-        assertTrue(
-                mResponseStep == ResponseStep.NOTHING
-                        || mResponseStep == ResponseStep.ON_STREAM_READY
-                        || mResponseStep == ResponseStep.ON_WRITE_COMPLETED);
-        assertNull(mError);
-
-        mResponseStep = ResponseStep.ON_RESPONSE_STARTED;
-        mResponseInfo = info;
-        if (maybeThrowCancelOrPause(stream, mReadStepBlock)) {
-            return;
-        }
-        startNextRead(stream);
-    }
-
-    @Override
-    public void onReadCompleted(
-            BidirectionalStream stream,
-            UrlResponseInfo info,
-            ByteBuffer byteBuffer,
-            boolean endOfStream) {
-        checkOnValidThread();
-        assertFalse(stream.isDone());
-        assertTrue(
-                mResponseStep == ResponseStep.ON_RESPONSE_STARTED
-                        || mResponseStep == ResponseStep.ON_READ_COMPLETED
-                        || mResponseStep == ResponseStep.ON_WRITE_COMPLETED
-                        || mResponseStep == ResponseStep.ON_TRAILERS);
-        assertNull(mError);
-
-        mResponseStep = ResponseStep.ON_READ_COMPLETED;
-        mResponseInfo = info;
-
-        final int bytesRead = byteBuffer.position() - mBufferPositionBeforeRead;
-        mHttpResponseDataLength += bytesRead;
-        final byte[] lastDataReceivedAsBytes = new byte[bytesRead];
-        // Rewind byteBuffer.position() to pre-read() position.
-        byteBuffer.position(mBufferPositionBeforeRead);
-        // This restores byteBuffer.position() to its value on entrance to
-        // this function.
-        byteBuffer.get(lastDataReceivedAsBytes);
-
-        mResponseAsString += new String(lastDataReceivedAsBytes);
-
-        if (maybeThrowCancelOrPause(stream, mReadStepBlock)) {
-            return;
-        }
-        // Do not read if EOF has been reached.
-        if (!endOfStream) {
-            startNextRead(stream);
-        }
-    }
-
-    @Override
-    public void onWriteCompleted(
-            BidirectionalStream stream,
-            UrlResponseInfo info,
-            ByteBuffer buffer,
-            boolean endOfStream) {
-        checkOnValidThread();
-        assertFalse(stream.isDone());
-        assertNull(mError);
-        mResponseStep = ResponseStep.ON_WRITE_COMPLETED;
-        mResponseInfo = info;
-        if (!mWriteBuffersToBeAcked.isEmpty()) {
-            assertEquals(buffer, mWriteBuffersToBeAcked.get(0).mBuffer);
-            mWriteBuffersToBeAcked.remove(0);
-        }
-        if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) {
-            return;
-        }
-        startNextWrite(stream);
-    }
-
-    @Override
-    public void onResponseTrailersReceived(
-            BidirectionalStream stream,
-            UrlResponseInfo info,
-            HeaderBlock trailers) {
-        checkOnValidThread();
-        assertFalse(stream.isDone());
-        assertNull(mError);
-        mResponseStep = ResponseStep.ON_TRAILERS;
-        mResponseInfo = info;
-        mTrailers = trailers;
-        if (maybeThrowCancelOrPause(stream, mReadStepBlock)) {
-            return;
-        }
-    }
-
-    @Override
-    public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) {
-        checkOnValidThread();
-        assertTrue(stream.isDone());
-        assertTrue(
-                mResponseStep == ResponseStep.ON_RESPONSE_STARTED
-                        || mResponseStep == ResponseStep.ON_READ_COMPLETED
-                        || mResponseStep == ResponseStep.ON_WRITE_COMPLETED
-                        || mResponseStep == ResponseStep.ON_TRAILERS);
-        assertFalse(mOnErrorCalled);
-        assertFalse(mOnCanceledCalled);
-        assertNull(mError);
-        assertEquals(0, mWriteBuffers.size());
-        assertEquals(0, mWriteBuffersToBeAcked.size());
-
-        mResponseStep = ResponseStep.ON_SUCCEEDED;
-        mResponseInfo = info;
-        openDone();
-        maybeThrowCancelOrPause(stream, mReadStepBlock);
-    }
-
-    @Override
-    public void onFailed(BidirectionalStream stream, UrlResponseInfo info, HttpException error) {
-        checkOnValidThread();
-        assertTrue(stream.isDone());
-        // Shouldn't happen after success.
-        assertTrue(mResponseStep != ResponseStep.ON_SUCCEEDED);
-        // Should happen at most once for a single stream.
-        assertFalse(mOnErrorCalled);
-        assertFalse(mOnCanceledCalled);
-        assertNull(mError);
-        mResponseStep = ResponseStep.ON_FAILED;
-        mResponseInfo = info;
-
-        mOnErrorCalled = true;
-        mError = error;
-        openDone();
-        maybeThrowCancelOrPause(stream, mReadStepBlock);
-    }
-
-    @Override
-    public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {
-        checkOnValidThread();
-        assertTrue(stream.isDone());
-        // Should happen at most once for a single stream.
-        assertFalse(mOnCanceledCalled);
-        assertFalse(mOnErrorCalled);
-        assertNull(mError);
-        mResponseStep = ResponseStep.ON_CANCELED;
-        mResponseInfo = info;
-
-        mOnCanceledCalled = true;
-        openDone();
-        maybeThrowCancelOrPause(stream, mReadStepBlock);
-    }
-
-    public void startNextRead(BidirectionalStream stream) {
-        startNextRead(stream, ByteBuffer.allocateDirect(READ_BUFFER_SIZE));
-    }
-
-    public void startNextRead(BidirectionalStream stream, ByteBuffer buffer) {
-        mBufferPositionBeforeRead = buffer.position();
-        stream.read(buffer);
-    }
-
-    public void startNextWrite(BidirectionalStream stream) {
-        if (!mWriteBuffers.isEmpty()) {
-            Iterator<WriteBuffer> iterator = mWriteBuffers.iterator();
-            while (iterator.hasNext()) {
-                WriteBuffer b = iterator.next();
-                stream.write(b.mBuffer, !iterator.hasNext());
-                iterator.remove();
-                if (b.mFlush) {
-                    stream.flush();
-                    break;
-                }
-            }
-        }
-    }
-
-    public boolean isDone() {
-        // It's not mentioned by the Android docs, but block(0) seems to block
-        // indefinitely, so have to block for one millisecond to get state
-        // without blocking.
-        return mDone.block(1);
-    }
-
-    /** Returns the number of pending Writes. */
-    public int numPendingWrites() {
-        return mWriteBuffers.size();
-    }
-
-    protected void openDone() {
-        mDone.open();
-    }
-
-    /** Returns {@code false} if the callback should continue to advance the stream. */
-    private boolean maybeThrowCancelOrPause(
-            final BidirectionalStream stream, ConditionVariable stepBlock) {
-        if (mResponseStep != mFailureStep || mFailureType == FailureType.NONE) {
-            if (!mAutoAdvance) {
-                stepBlock.open();
-                return true;
-            }
-            return false;
-        }
-
-        if (mFailureType == FailureType.THROW_SYNC) {
-            throw new IllegalStateException("Callback Exception.");
-        }
-        Runnable task =
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        stream.cancel();
-                    }
-                };
-        if (mFailureType == FailureType.CANCEL_ASYNC
-                || mFailureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE) {
-            getExecutor().execute(task);
-        } else {
-            task.run();
-        }
-        return mFailureType != FailureType.CANCEL_ASYNC_WITHOUT_PAUSE;
-    }
-
-    /** Checks whether callback methods are invoked on the correct thread. */
-    private void checkOnValidThread() {
-        if (!mUseDirectExecutor) {
-            assertEquals(mExecutorThread, Thread.currentThread());
-        }
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt b/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt
deleted file mode 100644
index 3a4486f..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 android.net.http.cts.util
-
-import android.net.http.UrlRequest.StatusListener
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.TimeUnit
-import org.junit.Assert.assertSame
-
-private const val TIMEOUT_MS = 12000L
-
-/** Test status listener for requests */
-class TestStatusListener : StatusListener {
-    private val statusFuture = CompletableFuture<Int>()
-
-    override fun onStatus(status: Int) {
-        statusFuture.complete(status)
-    }
-
-    /** Fails if the expected status is not the returned status */
-    fun expectStatus(expected: Int) {
-        assertSame(expected, statusFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS))
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java b/Cronet/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java
deleted file mode 100644
index 28443b7..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java
+++ /dev/null
@@ -1,481 +0,0 @@
-/*
- * 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 android.net.http.cts.util;
-
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.core.AnyOf.anyOf;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeThat;
-import static org.junit.Assume.assumeTrue;
-
-import android.net.http.CallbackException;
-import android.net.http.HttpException;
-import android.net.http.InlineExecutionProhibitedException;
-import android.net.http.UrlRequest;
-import android.net.http.UrlResponseInfo;
-import android.os.ConditionVariable;
-import android.os.StrictMode;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Callback that tracks information from different callbacks and has a
- * method to block thread until the request completes on another thread.
- * Allows us to cancel, block request or throw an exception from an arbitrary step.
- */
-public class TestUrlRequestCallback implements UrlRequest.Callback {
-    private static final int TIMEOUT_MS = 12_000;
-    public ArrayList<UrlResponseInfo> mRedirectResponseInfoList = new ArrayList<>();
-    public ArrayList<String> mRedirectUrlList = new ArrayList<>();
-    public UrlResponseInfo mResponseInfo;
-    public HttpException mError;
-
-    public ResponseStep mResponseStep = ResponseStep.NOTHING;
-
-    public int mRedirectCount;
-    public boolean mOnErrorCalled;
-    public boolean mOnCanceledCalled;
-
-    public int mHttpResponseDataLength;
-    public String mResponseAsString = "";
-
-    public int mReadBufferSize = 32 * 1024;
-
-    // When false, the consumer is responsible for all calls into the request
-    // that advance it.
-    private boolean mAutoAdvance = true;
-    // Whether an exception is thrown by maybeThrowCancelOrPause().
-    private boolean mCallbackExceptionThrown;
-
-    // Whether to permit calls on the network thread.
-    private boolean mAllowDirectExecutor;
-
-    // Whether to stop the executor thread after reaching a terminal method.
-    // Terminal methods are (onSucceeded, onFailed or onCancelled)
-    private boolean mBlockOnTerminalState;
-
-    // Conditionally fail on certain steps.
-    private FailureType mFailureType = FailureType.NONE;
-    private ResponseStep mFailureStep = ResponseStep.NOTHING;
-
-    // Signals when request is done either successfully or not.
-    private final ConditionVariable mDone = new ConditionVariable();
-
-    // Hangs the calling thread until a terminal method has started executing.
-    private final ConditionVariable mWaitForTerminalToStart = new ConditionVariable();
-
-    // Signaled on each step when mAutoAdvance is false.
-    private final ConditionVariable mStepBlock = new ConditionVariable();
-
-    // Executor Service for Http callbacks.
-    private final ExecutorService mExecutorService;
-    private Thread mExecutorThread;
-
-    // position() of ByteBuffer prior to read() call.
-    private int mBufferPositionBeforeRead;
-
-    private static class ExecutorThreadFactory implements ThreadFactory {
-        @Override
-        public Thread newThread(final Runnable r) {
-            return new Thread(new Runnable() {
-                @Override
-                public void run() {
-                    StrictMode.ThreadPolicy threadPolicy = StrictMode.getThreadPolicy();
-                    try {
-                        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
-                                .detectNetwork()
-                                .penaltyLog()
-                                .penaltyDeath()
-                                .build());
-                        r.run();
-                    } finally {
-                        StrictMode.setThreadPolicy(threadPolicy);
-                    }
-                }
-            });
-        }
-    }
-
-    public enum ResponseStep {
-        NOTHING,
-        ON_RECEIVED_REDIRECT,
-        ON_RESPONSE_STARTED,
-        ON_READ_COMPLETED,
-        ON_SUCCEEDED,
-        ON_FAILED,
-        ON_CANCELED,
-    }
-
-    public enum FailureType {
-        NONE,
-        CANCEL_SYNC,
-        CANCEL_ASYNC,
-        // Same as above, but continues to advance the request after posting
-        // the cancellation task.
-        CANCEL_ASYNC_WITHOUT_PAUSE,
-        THROW_SYNC
-    }
-
-    private static void assertContains(String expectedSubstring, String actualString) {
-        assertNotNull(actualString);
-        assertTrue("String [" + actualString + "] doesn't contain substring [" + expectedSubstring
-                + "]", actualString.contains(expectedSubstring));
-
-    }
-
-    /**
-     * Set {@code mExecutorThread}.
-     */
-    private void fillInExecutorThread() {
-        mExecutorService.execute(new Runnable() {
-            @Override
-            public void run() {
-                mExecutorThread = Thread.currentThread();
-            }
-        });
-    }
-
-    private boolean isTerminalCallback(ResponseStep step) {
-        switch (step) {
-            case ON_SUCCEEDED:
-            case ON_CANCELED:
-            case ON_FAILED:
-                return true;
-            default:
-                return false;
-        }
-    }
-
-    /**
-     * Create a {@link TestUrlRequestCallback} with a new single-threaded executor.
-     */
-    public TestUrlRequestCallback() {
-        this(Executors.newSingleThreadExecutor(new ExecutorThreadFactory()));
-    }
-
-    /**
-     * Create a {@link TestUrlRequestCallback} using a custom single-threaded executor.
-     */
-    public TestUrlRequestCallback(ExecutorService executorService) {
-        mExecutorService = executorService;
-        fillInExecutorThread();
-    }
-
-    /**
-     * This blocks the callback executor thread once it has reached a final state callback.
-     * In order to continue execution, this method must be called again and providing {@code false}
-     * to continue execution.
-     *
-     * @param blockOnTerminalState the state to set for the executor thread
-     */
-    public void setBlockOnTerminalState(boolean blockOnTerminalState) {
-        mBlockOnTerminalState = blockOnTerminalState;
-        if (!blockOnTerminalState) {
-            mDone.open();
-        }
-    }
-
-    public void setAutoAdvance(boolean autoAdvance) {
-        mAutoAdvance = autoAdvance;
-    }
-
-    public void setAllowDirectExecutor(boolean allowed) {
-        mAllowDirectExecutor = allowed;
-    }
-
-    public void setFailure(FailureType failureType, ResponseStep failureStep) {
-        mFailureStep = failureStep;
-        mFailureType = failureType;
-    }
-
-    /**
-     * Blocks the calling thread till callback execution is done
-     *
-     * @return true if the condition was opened, false if the call returns because of the timeout.
-     */
-    public boolean blockForDone() {
-        return mDone.block(TIMEOUT_MS);
-    }
-
-    /**
-     * Waits for a terminal callback to complete execution before failing if the callback
-     * is not the expected one
-     *
-     * @param expectedStep the expected callback step
-     */
-    public void expectCallback(ResponseStep expectedStep) {
-        if (isTerminalCallback(expectedStep)) {
-            assertTrue("Did not receive terminal callback before timeout", blockForDone());
-        }
-        assertSame(expectedStep, mResponseStep);
-    }
-
-    /**
-     * Waits for a terminal callback to complete execution before skipping the test if the
-     * callback is not the expected one
-     *
-     * @param expectedStep the expected callback step
-     */
-    public void assumeCallback(ResponseStep expectedStep) {
-        if (isTerminalCallback(expectedStep)) {
-            assumeTrue("Did not receive terminal callback before timeout", blockForDone());
-        }
-        assumeThat(expectedStep, equalTo(mResponseStep));
-    }
-
-    /**
-     * Blocks the calling thread until one of the final states has been called.
-     * This is called before the callback has finished executed.
-     */
-    public void waitForTerminalToStart() {
-        mWaitForTerminalToStart.block();
-    }
-
-    public void waitForNextStep() {
-        mStepBlock.block();
-        mStepBlock.close();
-    }
-
-    public ExecutorService getExecutor() {
-        return mExecutorService;
-    }
-
-    public void shutdownExecutor() {
-        mExecutorService.shutdown();
-    }
-
-    /**
-     * Shuts down the ExecutorService and waits until it executes all posted
-     * tasks.
-     */
-    public void shutdownExecutorAndWait() {
-        mExecutorService.shutdown();
-        try {
-            // Termination shouldn't take long. Use 1 min which should be more than enough.
-            mExecutorService.awaitTermination(1, TimeUnit.MINUTES);
-        } catch (InterruptedException e) {
-            fail("ExecutorService is interrupted while waiting for termination");
-        }
-        assertTrue(mExecutorService.isTerminated());
-    }
-
-    @Override
-    public void onRedirectReceived(
-            UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
-        checkExecutorThread();
-        assertFalse(request.isDone());
-        assertThat(mResponseStep, anyOf(
-                equalTo(ResponseStep.NOTHING),
-                equalTo(ResponseStep.ON_RECEIVED_REDIRECT)));
-        assertNull(mError);
-
-        mResponseStep = ResponseStep.ON_RECEIVED_REDIRECT;
-        mRedirectUrlList.add(newLocationUrl);
-        mRedirectResponseInfoList.add(info);
-        ++mRedirectCount;
-        if (maybeThrowCancelOrPause(request)) {
-            return;
-        }
-        request.followRedirect();
-    }
-
-    @Override
-    public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
-        checkExecutorThread();
-        assertFalse(request.isDone());
-        assertThat(mResponseStep, anyOf(
-                equalTo(ResponseStep.NOTHING),
-                equalTo(ResponseStep.ON_RECEIVED_REDIRECT)));
-        assertNull(mError);
-
-        mResponseStep = ResponseStep.ON_RESPONSE_STARTED;
-        mResponseInfo = info;
-        if (maybeThrowCancelOrPause(request)) {
-            return;
-        }
-        startNextRead(request);
-    }
-
-    @Override
-    public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
-        checkExecutorThread();
-        assertFalse(request.isDone());
-        assertThat(mResponseStep, anyOf(
-                equalTo(ResponseStep.ON_RESPONSE_STARTED),
-                equalTo(ResponseStep.ON_READ_COMPLETED)));
-        assertNull(mError);
-
-        mResponseStep = ResponseStep.ON_READ_COMPLETED;
-
-        final byte[] lastDataReceivedAsBytes;
-        final int bytesRead = byteBuffer.position() - mBufferPositionBeforeRead;
-        mHttpResponseDataLength += bytesRead;
-        lastDataReceivedAsBytes = new byte[bytesRead];
-        // Rewind |byteBuffer.position()| to pre-read() position.
-        byteBuffer.position(mBufferPositionBeforeRead);
-        // This restores |byteBuffer.position()| to its value on entrance to
-        // this function.
-        byteBuffer.get(lastDataReceivedAsBytes);
-        mResponseAsString += new String(lastDataReceivedAsBytes);
-
-        if (maybeThrowCancelOrPause(request)) {
-            return;
-        }
-        startNextRead(request);
-    }
-
-    @Override
-    public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
-        checkExecutorThread();
-        assertTrue(request.isDone());
-        assertThat(mResponseStep, anyOf(
-                equalTo(ResponseStep.ON_RESPONSE_STARTED),
-                equalTo(ResponseStep.ON_READ_COMPLETED)));
-        assertFalse(mOnErrorCalled);
-        assertFalse(mOnCanceledCalled);
-        assertNull(mError);
-
-        mResponseStep = ResponseStep.ON_SUCCEEDED;
-        mResponseInfo = info;
-        mWaitForTerminalToStart.open();
-        if (mBlockOnTerminalState) mDone.block();
-        openDone();
-        maybeThrowCancelOrPause(request);
-    }
-
-    @Override
-    public void onFailed(UrlRequest request, UrlResponseInfo info, HttpException error) {
-        // If the failure is because of prohibited direct execution, the test shouldn't fail
-        // since the request already did.
-        if (error.getCause() instanceof InlineExecutionProhibitedException) {
-            mAllowDirectExecutor = true;
-        }
-        checkExecutorThread();
-        assertTrue(request.isDone());
-        // Shouldn't happen after success.
-        assertNotEquals(ResponseStep.ON_SUCCEEDED, mResponseStep);
-        // Should happen at most once for a single request.
-        assertFalse(mOnErrorCalled);
-        assertFalse(mOnCanceledCalled);
-        assertNull(mError);
-        if (mCallbackExceptionThrown) {
-            assertTrue(error instanceof CallbackException);
-            assertContains("Exception received from UrlRequest.Callback", error.getMessage());
-            assertNotNull(error.getCause());
-            assertTrue(error.getCause() instanceof IllegalStateException);
-            assertContains("Listener Exception.", error.getCause().getMessage());
-        }
-
-        mResponseStep = ResponseStep.ON_FAILED;
-        mOnErrorCalled = true;
-        mError = error;
-        mWaitForTerminalToStart.open();
-        if (mBlockOnTerminalState) mDone.block();
-        openDone();
-        maybeThrowCancelOrPause(request);
-    }
-
-    @Override
-    public void onCanceled(UrlRequest request, UrlResponseInfo info) {
-        checkExecutorThread();
-        assertTrue(request.isDone());
-        // Should happen at most once for a single request.
-        assertFalse(mOnCanceledCalled);
-        assertFalse(mOnErrorCalled);
-        assertNull(mError);
-
-        mResponseStep = ResponseStep.ON_CANCELED;
-        mOnCanceledCalled = true;
-        mWaitForTerminalToStart.open();
-        if (mBlockOnTerminalState) mDone.block();
-        openDone();
-        maybeThrowCancelOrPause(request);
-    }
-
-    public void startNextRead(UrlRequest request) {
-        startNextRead(request, ByteBuffer.allocateDirect(mReadBufferSize));
-    }
-
-    public void startNextRead(UrlRequest request, ByteBuffer buffer) {
-        mBufferPositionBeforeRead = buffer.position();
-        request.read(buffer);
-    }
-
-    public boolean isDone() {
-        // It's not mentioned by the Android docs, but block(0) seems to block
-        // indefinitely, so have to block for one millisecond to get state
-        // without blocking.
-        return mDone.block(1);
-    }
-
-    protected void openDone() {
-        mDone.open();
-    }
-
-    private void checkExecutorThread() {
-        if (!mAllowDirectExecutor) {
-            assertEquals(mExecutorThread, Thread.currentThread());
-        }
-    }
-
-    /**
-     * Returns {@code false} if the listener should continue to advance the
-     * request.
-     */
-    private boolean maybeThrowCancelOrPause(final UrlRequest request) {
-        checkExecutorThread();
-        if (mResponseStep != mFailureStep || mFailureType == FailureType.NONE) {
-            if (!mAutoAdvance) {
-                mStepBlock.open();
-                return true;
-            }
-            return false;
-        }
-
-        if (mFailureType == FailureType.THROW_SYNC) {
-            assertFalse(mCallbackExceptionThrown);
-            mCallbackExceptionThrown = true;
-            throw new IllegalStateException("Listener Exception.");
-        }
-        Runnable task = new Runnable() {
-            @Override
-            public void run() {
-                request.cancel();
-            }
-        };
-        if (mFailureType == FailureType.CANCEL_ASYNC
-                || mFailureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE) {
-            getExecutor().execute(task);
-        } else {
-            task.run();
-        }
-        return mFailureType != FailureType.CANCEL_ASYNC_WITHOUT_PAUSE;
-    }
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt b/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt
deleted file mode 100644
index 7fc005a..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 android.net.http.cts.util
-
-import android.content.Context
-import android.net.ConnectivityManager
-import android.net.http.UrlResponseInfo
-import org.hamcrest.Matchers.equalTo
-import org.junit.Assert.assertEquals
-import org.junit.Assume.assumeNotNull
-import org.junit.Assume.assumeThat
-
-fun skipIfNoInternetConnection(context: Context) {
-    val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
-    assumeNotNull(
-        "This test requires a working Internet connection", connectivityManager!!.activeNetwork
-    )
-}
-
-fun assertOKStatusCode(info: UrlResponseInfo) {
-    assertEquals("Status code must be 200 OK", 200, info.httpStatusCode)
-}
-
-fun assumeOKStatusCode(info: UrlResponseInfo) {
-    assumeThat("Status code must be 200 OK", info.httpStatusCode, equalTo(200))
-}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java b/Cronet/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java
deleted file mode 100644
index 3b90fa0..0000000
--- a/Cronet/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * 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 android.net.http.cts.util;
-
-import android.net.http.UploadDataProvider;
-import android.net.http.UploadDataSink;
-import android.os.ParcelFileDescriptor;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.charset.StandardCharsets;
-
-/**
- * Provides implementations of {@link UploadDataProvider} for common use cases. Corresponds to
- * {@code android.net.http.apihelpers.UploadDataProviders} which is not an exposed API.
- */
-public final class UploadDataProviders {
-    /**
-     * Uploads an entire file.
-     *
-     * @param file The file to upload
-     * @return A new UploadDataProvider for the given file
-     */
-    public static UploadDataProvider create(final File file) {
-        return new FileUploadProvider(() -> new FileInputStream(file).getChannel());
-    }
-
-    /**
-     * Uploads an entire file, closing the descriptor when it is no longer needed.
-     *
-     * @param fd The file descriptor to upload
-     * @throws IllegalArgumentException if {@code fd} is not a file.
-     * @return A new UploadDataProvider for the given file descriptor
-     */
-    public static UploadDataProvider create(final ParcelFileDescriptor fd) {
-        return new FileUploadProvider(() -> {
-            if (fd.getStatSize() != -1) {
-                return new ParcelFileDescriptor.AutoCloseInputStream(fd).getChannel();
-            } else {
-                fd.close();
-                throw new IllegalArgumentException("Not a file: " + fd);
-            }
-        });
-    }
-
-    /**
-     * Uploads a ByteBuffer, from the current {@code buffer.position()} to {@code buffer.limit()}
-     *
-     * @param buffer The data to upload
-     * @return A new UploadDataProvider for the given buffer
-     */
-    public static UploadDataProvider create(ByteBuffer buffer) {
-        return new ByteBufferUploadProvider(buffer.slice());
-    }
-
-    /**
-     * Uploads {@code length} bytes from {@code data}, starting from {@code offset}
-     *
-     * @param data Array containing data to upload
-     * @param offset Offset within data to start with
-     * @param length Number of bytes to upload
-     * @return A new UploadDataProvider for the given data
-     */
-    public static UploadDataProvider create(byte[] data, int offset, int length) {
-        return new ByteBufferUploadProvider(ByteBuffer.wrap(data, offset, length).slice());
-    }
-
-    /**
-     * Uploads the contents of {@code data}
-     *
-     * @param data Array containing data to upload
-     * @return A new UploadDataProvider for the given data
-     */
-    public static UploadDataProvider create(byte[] data) {
-        return create(data, 0, data.length);
-    }
-
-    /**
-     * Uploads the UTF-8 representation of {@code data}
-     *
-     * @param data String containing data to upload
-     * @return A new UploadDataProvider for the given data
-     */
-    public static UploadDataProvider create(String data) {
-        return create(data.getBytes(StandardCharsets.UTF_8));
-    }
-
-    private interface FileChannelProvider {
-        FileChannel getChannel() throws IOException;
-    }
-
-    private static final class FileUploadProvider extends UploadDataProvider {
-        private volatile FileChannel mChannel;
-        private final FileChannelProvider mProvider;
-        /** Guards initialization of {@code mChannel} */
-        private final Object mLock = new Object();
-
-        private FileUploadProvider(FileChannelProvider provider) {
-            this.mProvider = provider;
-        }
-
-        @Override
-        public long getLength() throws IOException {
-            return getChannel().size();
-        }
-
-        @Override
-        public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException {
-            if (!byteBuffer.hasRemaining()) {
-                throw new IllegalStateException("Cronet passed a buffer with no bytes remaining");
-            }
-            FileChannel channel = getChannel();
-            int bytesRead = 0;
-            while (bytesRead == 0) {
-                int read = channel.read(byteBuffer);
-                if (read == -1) {
-                    break;
-                } else {
-                    bytesRead += read;
-                }
-            }
-            uploadDataSink.onReadSucceeded(false);
-        }
-
-        @Override
-        public void rewind(UploadDataSink uploadDataSink) throws IOException {
-            getChannel().position(0);
-            uploadDataSink.onRewindSucceeded();
-        }
-
-        /**
-         * Lazily initializes the channel so that a blocking operation isn't performed
-         * on a non-executor thread.
-         */
-        private FileChannel getChannel() throws IOException {
-            if (mChannel == null) {
-                synchronized (mLock) {
-                    if (mChannel == null) {
-                        mChannel = mProvider.getChannel();
-                    }
-                }
-            }
-            return mChannel;
-        }
-
-        @Override
-        public void close() throws IOException {
-            FileChannel channel = mChannel;
-            if (channel != null) {
-                channel.close();
-            }
-        }
-    }
-
-    private static final class ByteBufferUploadProvider extends UploadDataProvider {
-        private final ByteBuffer mUploadBuffer;
-
-        private ByteBufferUploadProvider(ByteBuffer uploadBuffer) {
-            this.mUploadBuffer = uploadBuffer;
-        }
-
-        @Override
-        public long getLength() {
-            return mUploadBuffer.limit();
-        }
-
-        @Override
-        public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) {
-            if (!byteBuffer.hasRemaining()) {
-                throw new IllegalStateException("Cronet passed a buffer with no bytes remaining");
-            }
-            if (byteBuffer.remaining() >= mUploadBuffer.remaining()) {
-                byteBuffer.put(mUploadBuffer);
-            } else {
-                int oldLimit = mUploadBuffer.limit();
-                mUploadBuffer.limit(mUploadBuffer.position() + byteBuffer.remaining());
-                byteBuffer.put(mUploadBuffer);
-                mUploadBuffer.limit(oldLimit);
-            }
-            uploadDataSink.onReadSucceeded(false);
-        }
-
-        @Override
-        public void rewind(UploadDataSink uploadDataSink) {
-            mUploadBuffer.position(0);
-            uploadDataSink.onRewindSucceeded();
-        }
-    }
-
-    // Prevent instantiation
-    private UploadDataProviders() {}
-}
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
deleted file mode 100644
index 9486e1f..0000000
--- a/Cronet/tests/mts/Android.bp
+++ /dev/null
@@ -1,68 +0,0 @@
-// 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 {
-    default_team: "trendy_team_fwk_core_networking",
-    // See: http://go/android-license-faq
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_genrule {
-    name: "net-http-test-jarjar-rules",
-    tool_files: [
-        ":NetHttpTestsLibPreJarJar{.jar}",
-        "jarjar_excludes.txt",
-    ],
-    tools: [
-        "jarjar-rules-generator",
-    ],
-    out: ["net_http_test_jarjar_rules.txt"],
-    cmd: "$(location jarjar-rules-generator) " +
-        "$(location :NetHttpTestsLibPreJarJar{.jar}) " +
-        "--prefix android.net.connectivity " +
-        "--excludes $(location jarjar_excludes.txt) " +
-        "--output $(out)",
-}
-
-// Library to be used in coverage tests. cronet_java_tests can't be used directly because the common
-// tests need to inherit the NetHttpTests manifest.
-android_library {
-    name: "NetHttpTestsLibPreJarJar",
-    static_libs: [
-        "cronet_aml_api_java",
-        "cronet_aml_java__testing",
-        "cronet_java_tests",
-    ],
-    sdk_version: "module_current",
-    min_sdk_version: "30",
-}
-
-android_test {
-    name: "NetHttpTests",
-    defaults: [
-        "mts-target-sdk-version-current",
-    ],
-    static_libs: ["NetHttpTestsLibPreJarJar"],
-    jarjar_rules: ":net-http-test-jarjar-rules",
-    jni_libs: [
-        "cronet_aml_components_cronet_android_cronet__testing",
-        "cronet_aml_components_cronet_android_cronet_tests__testing",
-        "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing",
-    ],
-    test_suites: [
-        "general-tests",
-        "mts-tethering",
-    ],
-    data: [":cronet_javatests_resources"],
-}
diff --git a/Cronet/tests/mts/AndroidManifest.xml b/Cronet/tests/mts/AndroidManifest.xml
deleted file mode 100644
index 2c56e3a..0000000
--- a/Cronet/tests/mts/AndroidManifest.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="android.net.http.mts">
-
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.INTERNET"/>
-
-    <application android:networkSecurityConfig="@xml/network_security_config">
-        <uses-library android:name="android.test.runner" />
-    </application>
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.net.http.mts"
-                     android:label="MTS tests of android.net.http">
-    </instrumentation>
-
-</manifest>
\ No newline at end of file
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
deleted file mode 100644
index bccbe29..0000000
--- a/Cronet/tests/mts/AndroidTest.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-<configuration description="Runs NetHttp Mainline Tests.">
-    <!-- Only run tests if the device under test is SDK version 30 or above. -->
-    <!-- TODO Switch back to Sdk30 when b/270049141 is fixed -->
-    <object type="module_controller"
-            class="com.android.tradefed.testtype.suite.module.Sdk31ModuleController" />
-
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="test-file-name" value="NetHttpTests.apk" />
-    </target_preparer>
-
-    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
-        <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" />
-        <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" />
-    </target_preparer>
-
-    <option name="test-tag" value="NetHttpTests" />
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="android.net.http.mts" />
-        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
-        <!-- b/298380508 -->
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
-        <!-- b/316571753 -->
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
-        <!-- b/316567693 -->
-        <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
-        <!-- b/316559294 -->
-        <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
-        <!-- b/316559294 -->
-        <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
-        <!-- b/316554711-->
-        <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
-        <!-- b/316550794 -->
-        <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
-        <option name="hidden-api-checks" value="false"/>
-        <option name="isolated-storage" value="false"/>
-    </test>
-
-    <!-- Only run NetHttpTests in MTS if the Tethering Mainline module is installed. -->
-    <object type="module_controller"
-            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
-        <option name="mainline-module-package-name" value="com.google.android.tethering" />
-    </object>
-</configuration>
\ No newline at end of file
diff --git a/Cronet/tests/mts/jarjar_excludes.txt b/Cronet/tests/mts/jarjar_excludes.txt
deleted file mode 100644
index b5cdf6e..0000000
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-# Exclude some test prefixes, as they can't be found after being jarjared.
-com\.android\.testutils\..+
-# jarjar-gen can't handle some kotlin object expression, exclude packages that include them
-androidx\..+
-# don't jarjar netty as it does JNI
-io\.netty\..+
-kotlin\.test\..+
-kotlin\.reflect\..+
-org\.mockito\..+
-# Do not jarjar the api classes
-android\.net\..+
-# cronet_tests.so is not jarjared and uses base classes. We can remove this when there's a
-# separate java base target to depend on.
-org\.chromium\.base\..+
-J\.cronet_tests_N(\$.+)?
-
-# don't jarjar automatically generated FooJni files.
-org\.chromium\.net\..+Jni(\$.+)?
-
-# Do not jarjar the tests and its utils as they also do JNI with cronet_tests.so
-org\.chromium\.net\..*Test.*(\$.+)?
-org\.chromium\.net\.NativeTestServer(\$.+)?
-org\.chromium\.net\.MockUrlRequestJobFactory(\$.+)?
-org\.chromium\.net\.QuicTestServer(\$.+)?
-org\.chromium\.net\.MockCertVerifier(\$.+)?
-org\.chromium\.net\.LogcatCapture(\$.+)?
-org\.chromium\.net\.ReportingCollector(\$.+)?
-org\.chromium\.net\.Http2TestServer(\$.+)?
-org\.chromium\.net\.Http2TestHandler(\$.+)?
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/raw/quicroot.pem b/Cronet/tests/mts/res/raw/quicroot.pem
deleted file mode 100644
index af21b3e..0000000
--- a/Cronet/tests/mts/res/raw/quicroot.pem
+++ /dev/null
@@ -1,19 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIC/jCCAeagAwIBAgIUXOi6XoxnMUjJg4jeOwRhsdqEqEQwDQYJKoZIhvcNAQEL
-BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTIzMDYwMTExMjcwMFoXDTMz
-MDUyOTExMjcwMFowFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjANBgkqhkiG
-9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl9xCMPMIvfmJWz25AG/VtgWbqNs67HXQbXWf
-pDF2wjQpHVOYbfl7Zgly5O+5es1aUbJaGyZ9G6xuYSXKFnnYLoP7M86O05fQQBAj
-K+IE5nO6136ksCAfxCFTFfn4vhPvK8Vba5rqox4WeIXYKvHYSoiHz0ELrnFOHcyN
-Innyze7bLtkMCA1ShHpmvDCR+U3Uj6JwOfoirn29jjU/48/ORha7dcJYtYXk2eGo
-RJfrtIx20tXAaKaGnXOCGYbEVXTeQkQPqKFVzqP7+KYS/Y8eNFV35ugpLNES+44T
-bQ2QruTZdrNRjJkEoyiB/E53a0OUltB/R7Z0L0xstnKfsAf3OwIDAQABo0IwQDAP
-BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUVdXNh2lk
-51/6hMmz0Z+OpIe8+f0wDQYJKoZIhvcNAQELBQADggEBADNg7G8n6DUrQ5doXzm9
-kOp5siX6iPs0zFReXKhIT1Gef63l3tb7AdPedF03aj9XkUt0shhNOGG5SK2k5KBQ
-MJc9muYRCAyo2xMr3rFUQdI5B51SCy5HeAMralgTHXN0Hv+TH04YfRrACVmr+5ke
-pH3bF1gYaT+Zy5/pHJnV5lcwS6/H44g9XXWIopjWCwbfzKxIuWofqL4fiToPSIYu
-MCUI4bKZipcJT5O6rdz/S9lbgYVjOJ4HAoT2icNQqNMMfULKevmF8SdJzfNd35yn
-tAKTROhIE2aQRVCclrjo/T3eyjWGGoJlGmxKbeCf/rXzcn1BRtk/UzLnbUFFlg5l
-axw=
------END CERTIFICATE-----
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
deleted file mode 100644
index 48ce420..0000000
--- a/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-
-<resources>
-    <bool name="is_running_in_aosp">true</bool>
-</resources>
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/xml/network_security_config.xml b/Cronet/tests/mts/res/xml/network_security_config.xml
deleted file mode 100644
index 32b7171..0000000
--- a/Cronet/tests/mts/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  ~ 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.
-  -->
-
-<network-security-config>
-  <base-config>
-    <trust-anchors>
-      <certificates src="@raw/quicroot"/>
-      <certificates src="system"/>
-    </trust-anchors>
-  </base-config>
-  <!-- Since Android 9 (API 28) cleartext support is disabled by default, this
-       causes some of our tests to fail (see crbug/1220357).
-       The following configs allow http requests for the domains used in these
-       tests.
-
-       TODO(stefanoduo): Figure out if we really need to use http for these tests
-  -->
-  <domain-config cleartextTrafficPermitted="true">
-    <!-- Used as the base URL by native test server (net::EmbeddedTestServer) -->
-    <domain includeSubdomains="true">127.0.0.1</domain>
-    <!-- Used by CronetHttpURLConnectionTest#testIOExceptionInterruptRethrown -->
-    <domain includeSubdomains="true">localhost</domain>
-    <!-- Used by CronetHttpURLConnectionTest#testBadIP -->
-    <domain includeSubdomains="true">0.0.0.0</domain>
-    <!-- Used by CronetHttpURLConnectionTest#testSetUseCachesFalse -->
-    <domain includeSubdomains="true">host-cache-test-host</domain>
-    <!-- Used by CronetHttpURLConnectionTest#testBadHostname -->
-    <domain includeSubdomains="true">this-weird-host-name-does-not-exist</domain>
-    <!-- Used by CronetUrlRequestContextTest#testHostResolverRules -->
-    <domain includeSubdomains="true">some-weird-hostname</domain>
-  </domain-config>
-</network-security-config>
\ No newline at end of file
diff --git a/Cronet/tools/import/copy.bara.sky b/Cronet/tools/import/copy.bara.sky
deleted file mode 100644
index 61e3ba4..0000000
--- a/Cronet/tools/import/copy.bara.sky
+++ /dev/null
@@ -1,127 +0,0 @@
-# Copyright 2023 Google Inc. All rights reserved.
-#
-# 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.
-
-common_excludes = [
-    # Exclude all Android build files
-    "**/Android.bp",
-    "**/Android.mk",
-
-    # Exclude existing *OWNERS files
-    "**/*OWNERS",
-    "**/.git/**",
-    "**/.gitignore",
-]
-
-cronet_origin_files = glob(
-    include = [
-        "base/**",
-        "build/**",
-        "build/buildflag.h",
-        "chrome/VERSION",
-        "components/cronet/**",
-        "components/metrics/**",
-        "components/nacl/**",
-        "components/prefs/**",
-        "crypto/**",
-        "ipc/**",
-        "net/**",
-        # Note: Only used for tests.
-        "testing/**",
-        "url/**",
-        "LICENSE",
-    ],
-    exclude = common_excludes + [
-        # Per aosp/2367109
-        "build/android/CheckInstallApk-debug.apk",
-        "build/android/unused_resources/**",
-        "build/linux/**",
-
-        # Per aosp/2374766
-        "components/cronet/ios/**",
-        "components/cronet/native/**",
-
-        # Per aosp/2399270
-        "testing/buildbot/**",
-
-        # Exclude all third-party directories. Those are specified explicitly
-        # below, so no dependency can accidentally creep in.
-        "**/third_party/**",
-    ],
-) + glob(
-    # Explicitly include third-party dependencies.
-    # Note: some third-party dependencies include a third_party folder within
-    # them. So far, this has not become a problem.
-    include = [
-        "base/third_party/cityhash/**",
-        "base/third_party/cityhash_v103/**",
-        "base/third_party/double_conversion/**",
-        "base/third_party/dynamic_annotations/**",
-        "base/third_party/icu/**",
-        "base/third_party/nspr/**",
-        "base/third_party/superfasthash/**",
-        "base/third_party/valgrind/**",
-        # Those are temporarily needed until Chromium finish the migration
-        # of libc++[abi]
-        "buildtools/third_party/libc++/**",
-        "buildtools/third_party/libc++abi/**",
-        # Note: Only used for tests.
-        "net/third_party/nist-pkits/**",
-        "net/third_party/quiche/**",
-        "net/third_party/uri_template/**",
-        "third_party/abseil-cpp/**",
-        "third_party/android_ndk/sources/android/cpufeatures/**",
-        "third_party/ashmem/**",
-        "third_party/boringssl/**",
-        "third_party/brotli/**",
-        # Note: Only used for tests.
-        "third_party/ced/**",
-        "third_party/cpu_features/**",
-        # Note: Only used for tests.
-        "third_party/google_benchmark/**",
-        # Note: Only used for tests.
-        "third_party/googletest/**",
-        "third_party/icu/**",
-        "third_party/jni_zero/**",
-        "third_party/libc++/**",
-        "third_party/libc++abi/**",
-        "third_party/libevent/**",
-        # Note: Only used for tests.
-        "third_party/libxml/**",
-        # Note: Only used for tests.
-        "third_party/lss/**",
-        "third_party/metrics_proto/**",
-        "third_party/modp_b64/**",
-        "third_party/protobuf/**",
-        # Note: Only used for tests.
-        "third_party/quic_trace/**",
-        # Note: Cronet currently uses Android's zlib
-        # "third_party/zlib/**",
-        "url/third_party/mozilla/**",
-    ],
-    exclude = common_excludes,
-)
-
-core.workflow(
-    name = "import_cronet",
-    authoring = authoring.overwrite("Cronet Mainline Eng <cronet-mainline-eng+copybara@google.com>"),
-    # Origin folder is specified via source_ref argument, see import_cronet.sh
-    origin = folder.origin(),
-    origin_files = cronet_origin_files,
-    destination = git.destination(
-        # The destination URL is set by the invoking script.
-        url = "overwritten/by/script",
-        push = "upstream-import",
-    ),
-    mode = "SQUASH",
-)
diff --git a/Cronet/tools/import/import_cronet.sh b/Cronet/tools/import/import_cronet.sh
deleted file mode 100755
index 0f04af7..0000000
--- a/Cronet/tools/import/import_cronet.sh
+++ /dev/null
@@ -1,146 +0,0 @@
-#!/bin/bash
-
-# Copyright 2023 Google Inc. All rights reserved.
-#
-# 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.
-
-# Script to invoke copybara locally to import Cronet into Android.
-# Inputs:
-#  Environment:
-#   ANDROID_BUILD_TOP: path the root of the current Android directory.
-#  Arguments:
-#   -l rev: The last revision that was imported.
-#  Optional Arguments:
-#   -n rev: The new revision to import.
-#   -f: Force copybara to ignore a failure to find the last imported revision.
-
-set -e -x
-
-OPTSTRING=fl:n:
-
-usage() {
-    cat <<EOF
-Usage: import_cronet.sh -n new-rev [-l last-rev] [-f]
-EOF
-    exit 1
-}
-
-COPYBARA_FOLDER_ORIGIN="/tmp/copybara-origin"
-
-#######################################
-# Create local upstream-import branch in external/cronet.
-# Globals:
-#   ANDROID_BUILD_TOP
-# Arguments:
-#   none
-#######################################
-setup_upstream_import_branch() {
-    local git_dir="${ANDROID_BUILD_TOP}/external/cronet"
-
-    (cd "${git_dir}" && git fetch aosp upstream-import:upstream-import)
-}
-
-#######################################
-# Setup folder.origin for copybara inside /tmp
-# Globals:
-#   COPYBARA_FOLDER_ORIGIN
-# Arguments:
-#   new_rev, string
-#######################################
-setup_folder_origin() (
-    local _new_rev=$1
-    mkdir -p "${COPYBARA_FOLDER_ORIGIN}"
-    cd "${COPYBARA_FOLDER_ORIGIN}"
-
-    if [ -d src ]; then
-        (cd src && git fetch --tags && git checkout "${_new_rev}")
-    else
-        # For this to work _new_rev must be a branch or a tag.
-        git clone --depth=1 --branch "${_new_rev}" https://chromium.googlesource.com/chromium/src.git
-    fi
-
-
-    cat <<EOF >.gclient
-solutions = [
-  {
-    "name": "src",
-    "url": "https://chromium.googlesource.com/chromium/src.git",
-    "managed": False,
-    "custom_deps": {},
-    "custom_vars": {},
-  },
-]
-target_os = ["android"]
-EOF
-    cd src
-    # Set appropriate gclient flags to speed up syncing.
-    gclient sync \
-        --no-history \
-        --shallow \
-        --delete_unversioned_trees
-)
-
-#######################################
-# Runs the copybara import of Chromium
-# Globals:
-#   ANDROID_BUILD_TOP
-#   COPYBARA_FOLDER_ORIGIN
-# Arguments:
-#   last_rev, string or empty
-#   force, string or empty
-#######################################
-do_run_copybara() {
-    local _last_rev=$1
-    local _force=$2
-
-    local -a flags
-    flags+=(--git-destination-url="file://${ANDROID_BUILD_TOP}/external/cronet")
-    flags+=(--repo-timeout 3m)
-
-    # buildtools/third_party/libc++ contains an invalid symlink
-    flags+=(--folder-origin-ignore-invalid-symlinks)
-    flags+=(--git-no-verify)
-
-    if [ ! -z "${_force}" ]; then
-        flags+=(--force)
-    fi
-
-    if [ ! -z "${_last_rev}" ]; then
-        flags+=(--last-rev "${_last_rev}")
-    fi
-
-    /google/bin/releases/copybara/public/copybara/copybara \
-        "${flags[@]}" \
-        "${ANDROID_BUILD_TOP}/packages/modules/Connectivity/Cronet/tools/import/copy.bara.sky" \
-        import_cronet "${COPYBARA_FOLDER_ORIGIN}/src"
-}
-
-while getopts $OPTSTRING opt; do
-    case "${opt}" in
-        f) force=true ;;
-        l) last_rev="${OPTARG}" ;;
-        n) new_rev="${OPTARG}" ;;
-        ?) usage ;;
-        *) echo "'${opt}' '${OPTARG}'"
-    esac
-done
-
-if [ -z "${new_rev}" ]; then
-    echo "-n argument required"
-    usage
-fi
-
-setup_upstream_import_branch
-setup_folder_origin "${new_rev}"
-do_run_copybara "${last_rev}" "${force}"
-
diff --git a/DnsResolver/DnsBpfHelper.cpp b/DnsResolver/DnsBpfHelper.cpp
index de8bef5..0719ade 100644
--- a/DnsResolver/DnsBpfHelper.cpp
+++ b/DnsResolver/DnsBpfHelper.cpp
@@ -69,9 +69,10 @@
   // state, making it a trustworthy source. Since this library primarily serves DNS resolvers,
   // relying solely on V+ data prevents erroneous blocking of DNS queries.
   if (android::modules::sdklevel::IsAtLeastV() && metered) {
-    // The background data setting (PENALTY_BOX_MATCH) and unrestricted data usage setting
-    // (HAPPY_BOX_MATCH) for individual apps override the system wide Data Saver setting.
-    if (uidRules & PENALTY_BOX_MATCH) return true;
+    // The background data setting (PENALTY_BOX_USER_MATCH, PENALTY_BOX_ADMIN_MATCH) and
+    // unrestricted data usage setting (HAPPY_BOX_MATCH) for individual apps override the system
+    // wide Data Saver setting.
+    if (uidRules & (PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH)) return true;
     if (uidRules & HAPPY_BOX_MATCH) return false;
 
     auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/DnsResolver/DnsBpfHelperTest.cpp
index 67b5b95..18a5df4 100644
--- a/DnsResolver/DnsBpfHelperTest.cpp
+++ b/DnsResolver/DnsBpfHelperTest.cpp
@@ -158,23 +158,33 @@
     }
   } testConfigs[]{
     // clang-format off
-    // enabledRules, dataSaverEnabled, uidRules,                                        blocked
-    {NO_MATCH,       false,            NO_MATCH,                                        false},
-    {NO_MATCH,       false,            PENALTY_BOX_MATCH,                               true},
-    {NO_MATCH,       false,            HAPPY_BOX_MATCH,                                 false},
-    {NO_MATCH,       false,            PENALTY_BOX_MATCH|HAPPY_BOX_MATCH,               true},
-    {NO_MATCH,       true,             NO_MATCH,                                        true},
-    {NO_MATCH,       true,             PENALTY_BOX_MATCH,                               true},
-    {NO_MATCH,       true,             HAPPY_BOX_MATCH,                                 false},
-    {NO_MATCH,       true,             PENALTY_BOX_MATCH|HAPPY_BOX_MATCH,               true},
-    {STANDBY_MATCH,  false,            STANDBY_MATCH,                                   true},
-    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_MATCH,                 true},
-    {STANDBY_MATCH,  false,            STANDBY_MATCH|HAPPY_BOX_MATCH,                   true},
-    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
-    {STANDBY_MATCH,  true,             STANDBY_MATCH,                                   true},
-    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_MATCH,                 true},
-    {STANDBY_MATCH,  true,             STANDBY_MATCH|HAPPY_BOX_MATCH,                   true},
-    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
+    // enabledRules, dataSaverEnabled, uidRules,                                            blocked
+    {NO_MATCH,       false,            NO_MATCH,                                             false},
+    {NO_MATCH,       false,            PENALTY_BOX_USER_MATCH,                                true},
+    {NO_MATCH,       false,            PENALTY_BOX_ADMIN_MATCH,                               true},
+    {NO_MATCH,       false,            PENALTY_BOX_USER_MATCH|PENALTY_BOX_ADMIN_MATCH,        true},
+    {NO_MATCH,       false,            HAPPY_BOX_MATCH,                                      false},
+    {NO_MATCH,       false,            PENALTY_BOX_USER_MATCH|HAPPY_BOX_MATCH,                true},
+    {NO_MATCH,       false,            PENALTY_BOX_ADMIN_MATCH|HAPPY_BOX_MATCH,               true},
+    {NO_MATCH,       true,             NO_MATCH,                                              true},
+    {NO_MATCH,       true,             PENALTY_BOX_USER_MATCH,                                true},
+    {NO_MATCH,       true,             PENALTY_BOX_ADMIN_MATCH,                               true},
+    {NO_MATCH,       true,             PENALTY_BOX_USER_MATCH|PENALTY_BOX_ADMIN_MATCH,        true},
+    {NO_MATCH,       true,             HAPPY_BOX_MATCH,                                      false},
+    {NO_MATCH,       true,             PENALTY_BOX_USER_MATCH|HAPPY_BOX_MATCH,                true},
+    {NO_MATCH,       true,             PENALTY_BOX_ADMIN_MATCH|HAPPY_BOX_MATCH,               true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH,                                         true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_USER_MATCH,                  true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_ADMIN_MATCH,                 true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|HAPPY_BOX_MATCH,                         true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_USER_MATCH|HAPPY_BOX_MATCH,  true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_ADMIN_MATCH|HAPPY_BOX_MATCH, true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH,                                         true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_USER_MATCH,                  true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_ADMIN_MATCH,                 true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|HAPPY_BOX_MATCH,                         true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_USER_MATCH|HAPPY_BOX_MATCH,  true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_ADMIN_MATCH|HAPPY_BOX_MATCH, true},
     // clang-format on
   };
 
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 83f798a..6d8ed4a 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,10 +1,6 @@
-chiachangwang@google.com
-cken@google.com
 jchalard@google.com
 junyulai@google.com
-lifr@google.com
 lorenzo@google.com
-markchien@google.com
 martinwu@google.com
 maze@google.com
 motomuman@google.com
@@ -12,7 +8,5 @@
 prohr@google.com
 reminv@google.com
 satk@google.com
-waynema@google.com
 xiaom@google.com
-yumike@google.com
 yuyanghuang@google.com
diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts
index 7612210..9e4e4a1 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -1,9 +1,12 @@
 lorenzo@google.com
 satk@google.com #{LAST_RESORT_SUGGESTION}
 
-# For cherry-picks of CLs that are already merged in aosp/master, or flaky test fixes.
+# For cherry-picks of CLs that are already merged in aosp/master, flaky test
+# fixes, or no-op refactors.
 jchalard@google.com #{LAST_RESORT_SUGGESTION}
+# In addition to cherry-picks, flaky test fixes and no-op refactors, also for
+# APF firmware tests (to verify correct behaviour of the wifi APF interpreter)
 maze@google.com #{LAST_RESORT_SUGGESTION}
-# In addition to cherry-picks and flaky test fixes, also for incremental changes on NsdManager tests
-# to increase coverage for existing behavior, and testing of bug fixes in NsdManager
+# In addition to cherry-picks, flaky test fixes and no-op refactors, also for
+# NsdManager tests
 reminv@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 83619d6..39009cb 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,6 +1,15 @@
+[Builtin Hooks]
+bpfmt = true
+clang_format = true
+ktfmt = true
+
+[Builtin Hooks Options]
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,hpp
+ktfmt = --kotlinlang-style
+
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
 
 hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ab3ed66..bcf5e8b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,4 +1,140 @@
 {
+  "captiveportal-networkstack-resolve-tethering-mainline-presubmit": [
+    {
+      "name": "CtsNetTestCases",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    {
+      "name": "CtsNetTestCasesMaxTargetSdk30",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    {
+      "name": "CtsNetTestCasesMaxTargetSdk31",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    {
+      "name": "CtsNetTestCasesMaxTargetSdk33",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
+    {
+      "name": "bpf_existence_test"
+    },
+    {
+      "name": "connectivity_native_test"
+    },
+    {
+      "name": "netd_updatable_unit_test"
+    },
+    {
+      "name": "ConnectivityCoverageTests",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        }
+      ]
+    },
+    {
+      "name": "libnetworkstats_test"
+    },
+    {
+      "name": "CtsTetheringTestLatestSdk",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+        }
+      ]
+    }
+  ],
+  "captiveportal-networkstack-mainline-presubmit": [
+    // Test with APK modules only, in cases where APEX is not supported, or the other modules
+    // were simply not updated
+    {
+      "name": "CtsNetTestCases",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.ConnectivityModuleTest"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.DnsResolverModuleTest"
+        }
+      ]
+    }
+  ],
+  "tethering-mainline-presubmit": [
+    // Test with connectivity/tethering module only, to catch integration issues with older versions
+    // of other modules. "new tethering + old NetworkStack" is not a configuration that should
+    // really exist in the field, but there is no strong guarantee, and it is required by MTS
+    // testing for module qualification, where modules are tested independently.
+    {
+      "name": "CtsNetTestCases",
+      "options": [
+        {
+          "exclude-annotation": "com.android.testutils.SkipPresubmit"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.SkipMainlinePresubmit"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.DnsResolverModuleTest"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+        }
+      ]
+    }
+  ],
   "presubmit": [
     {
       "name": "ConnectivityCoverageTests",
@@ -22,8 +158,6 @@
     },
     // Run in addition to mainline-presubmit as mainline-presubmit is not
     // supported in every branch.
-    // CtsNetTestCasesLatestSdk uses stable API shims, so does not exercise
-    // some latest APIs. Run CtsNetTestCases to get coverage of newer APIs.
     {
       "name": "CtsNetTestCases",
       "options": [
@@ -35,18 +169,6 @@
         }
       ]
     },
-    // Also run CtsNetTestCasesLatestSdk to ensure tests using older shims pass.
-    {
-      "name": "CtsNetTestCasesLatestSdk",
-      "options": [
-        {
-          "exclude-annotation": "com.android.testutils.SkipPresubmit"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.RequiresDevice"
-        }
-      ]
-    },
     // CTS tests that target older SDKs.
     {
       "name": "CtsNetTestCasesMaxTargetSdk30",
@@ -104,19 +226,6 @@
     },
     {
       "name": "FrameworksNetIntegrationTests"
-    },
-    // Runs both NetHttpTests and CtsNetHttpTestCases
-    {
-      "name": "NetHttpCoverageTests",
-      "options": [
-        {
-          "exclude-annotation": "com.android.testutils.SkipPresubmit"
-        },
-        {
-          // These sometimes take longer than 1 min which is the presubmit timeout
-          "exclude-annotation": "androidx.test.filters.LargeTest"
-        }
-      ]
     }
   ],
   "postsubmit": [
@@ -145,13 +254,14 @@
     {
       "name": "FrameworksNetTests"
     },
+    // TODO: Move to presumit after meet SLO requirement.
     {
-      "name": "NetHttpCoverageTests"
+      "name": "NetworkStaticLibHostPythonTests"
     }
   ],
   "mainline-presubmit": [
     {
-      "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "options": [
         {
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -209,7 +319,7 @@
     // Test with APK modules only, in cases where APEX is not supported, or the other modules
     // were simply not updated
     {
-      "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk]",
+      "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk]",
       "options": [
         {
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -233,7 +343,7 @@
     // really exist in the field, but there is no strong guarantee, and it is required by MTS
     // testing for module qualification, where modules are tested independently.
     {
-      "name": "CtsNetTestCasesLatestSdk[com.google.android.tethering.apex]",
+      "name": "CtsNetTestCases[com.google.android.tethering.apex]",
       "options": [
         {
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
@@ -246,6 +356,9 @@
         },
         {
           "exclude-annotation": "com.android.testutils.DnsResolverModuleTest"
+        },
+        {
+          "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
         }
       ]
     },
@@ -270,18 +383,6 @@
       "name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     },
     {
-      "name": "NetHttpCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
-      "options": [
-        {
-          "exclude-annotation": "com.android.testutils.SkipPresubmit"
-        },
-        {
-          // These sometimes take longer than 1 min which is the presubmit timeout
-          "exclude-annotation": "androidx.test.filters.LargeTest"
-        }
-      ]
-    },
-    {
       "name": "CtsTetheringTestLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "options": [
         {
@@ -293,7 +394,7 @@
   "mainline-postsubmit": [
     // Tests on physical devices with SIM cards: postsubmit only for capacity constraints
     {
-      "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "keywords": ["sim"]
     },
     {
@@ -307,7 +408,7 @@
     },
     // Postsubmit on virtual devices to monitor flakiness of @SkipMainlinePresubmit methods
     {
-      "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "name": "CtsNetTestCases[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "options": [
         {
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
@@ -329,6 +430,9 @@
       "path": "packages/modules/CaptivePortalLogin"
     },
     {
+      "path": "external/cronet"
+    },
+    {
       "path": "vendor/xts/gts-tests/hostsidetests/networkstack"
     }
   ]
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index e4e6c70..d04660d 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -69,11 +69,7 @@
         "android.hardware.tetheroffload.control-V1.0-java",
         "android.hardware.tetheroffload.control-V1.1-java",
         "android.hidl.manager-V1.2-java",
-        "net-utils-framework-common",
-        "net-utils-device-common",
-        "net-utils-device-common-bpf",
-        "net-utils-device-common-ip",
-        "net-utils-device-common-netlink",
+        "net-utils-connectivity-apks",
         "netd-client",
         "tetheringstatsprotos",
     ],
@@ -98,7 +94,6 @@
     ],
     static_libs: [
         "NetworkStackApiCurrentShims",
-        "net-utils-device-common-struct",
     ],
     apex_available: ["com.android.tethering"],
     lint: {
@@ -115,7 +110,6 @@
     ],
     static_libs: [
         "NetworkStackApiStableShims",
-        "net-utils-device-common-struct",
     ],
     apex_available: ["com.android.tethering"],
     lint: {
@@ -126,7 +120,7 @@
 // Due to b/143733063, APK can't access a jni lib that is in APEX (but not in the APK).
 cc_library {
     name: "libcom_android_networkstack_tethering_util_jni",
-    sdk_version: "30",
+    sdk_version: "current",
     apex_available: [
         "com.android.tethering",
     ],
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 30bdf37..8ed5ac0 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -103,16 +103,17 @@
         "dscpPolicy.o",
         "netd.o",
         "offload.o",
-        "offload@btf.o",
+        "offload@mainline.o",
         "test.o",
-        "test@btf.o",
+        "test@mainline.o",
     ],
     apps: [
         "ServiceConnectivityResources",
     ],
     prebuilts: [
         "current_sdkinfo",
-        "netbpfload.mainline.rc",
+        "netbpfload.33rc",
+        "netbpfload.35rc",
         "ot-daemon.init.34rc",
     ],
     manifest: "manifest.json",
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 9fa073b..39a7540 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -58,6 +58,7 @@
     permitted_packages: ["android.net"],
     lint: {
         strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
     },
     aconfig_declarations: [
         "com.android.net.flags-aconfig",
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index a287b42..cccafd5 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -47,6 +47,7 @@
     field public static final int TETHERING_INVALID = -1; // 0xffffffff
     field public static final int TETHERING_NCM = 4; // 0x4
     field public static final int TETHERING_USB = 1; // 0x1
+    field @FlaggedApi("com.android.net.flags.tethering_request_virtual") public static final int TETHERING_VIRTUAL = 7; // 0x7
     field public static final int TETHERING_WIFI = 0; // 0x0
     field public static final int TETHERING_WIFI_P2P = 3; // 0x3
     field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
diff --git a/Tethering/common/TetheringLib/lint-baseline.xml b/Tethering/common/TetheringLib/lint-baseline.xml
new file mode 100644
index 0000000..5171efb
--- /dev/null
+++ b/Tethering/common/TetheringLib/lint-baseline.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="Field `TETHERING_VIRTUAL` is a flagged API and should be inside an `if (Flags.tetheringRequestVirtual())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL) to transfer requirement to caller`)"
+        errorLine1="    public static final int MAX_TETHERING_TYPE = TETHERING_VIRTUAL;"
+        errorLine2="                                                 ~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/Tethering/common/TetheringLib/src/android/net/TetheringManager.java"
+            line="211"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `TetheringRequest()` is a flagged API and should be inside an `if (Flags.tetheringRequestWithSoftApConfig())` check (or annotate the surrounding method `build` with `@FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG) to transfer requirement to caller`)"
+        errorLine1="                return new TetheringRequest(mBuilderParcel);"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/Tethering/common/TetheringLib/src/android/net/TetheringManager.java"
+            line="814"
+            column="24"/>
+    </issue>
+
+</issues>
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 7b769d4..0f5a014 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -68,6 +68,8 @@
     public static class Flags {
         static final String TETHERING_REQUEST_WITH_SOFT_AP_CONFIG =
                 "com.android.net.flags.tethering_request_with_soft_ap_config";
+        static final String TETHERING_REQUEST_VIRTUAL =
+                "com.android.net.flags.tethering_request_virtual";
     }
 
     private static final String TAG = TetheringManager.class.getSimpleName();
@@ -195,10 +197,22 @@
     public static final int TETHERING_WIGIG = 6;
 
     /**
+     * VIRTUAL tethering type.
+     *
+     * This tethering type is for providing external network to virtual machines
+     * running on top of Android devices, which are created and managed by
+     * AVF(Android Virtualization Framework).
+     * @hide
+     */
+    @FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL)
+    @SystemApi
+    public static final int TETHERING_VIRTUAL = 7;
+
+    /**
      * The int value of last tethering type.
      * @hide
      */
-    public static final int MAX_TETHERING_TYPE = TETHERING_WIGIG;
+    public static final int MAX_TETHERING_TYPE = TETHERING_VIRTUAL;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -688,7 +702,11 @@
         /** A configuration set for TetheringRequest. */
         private final TetheringRequestParcel mRequestParcel;
 
-        private TetheringRequest(@NonNull final TetheringRequestParcel request) {
+        /**
+         * @hide
+         */
+        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        public TetheringRequest(@NonNull final TetheringRequestParcel request) {
             mRequestParcel = request;
         }
 
@@ -891,6 +909,28 @@
                     + mRequestParcel.exemptFromEntitlementCheck + ", showProvisioningUi= "
                     + mRequestParcel.showProvisioningUi + " ]";
         }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (!(obj instanceof TetheringRequest otherRequest)) return false;
+            TetheringRequestParcel parcel = getParcel();
+            TetheringRequestParcel otherParcel = otherRequest.getParcel();
+            return parcel.tetheringType == otherParcel.tetheringType
+                    && Objects.equals(parcel.localIPv4Address, otherParcel.localIPv4Address)
+                    && Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
+                    && parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
+                    && parcel.showProvisioningUi == otherParcel.showProvisioningUi
+                    && parcel.connectivityScope == otherParcel.connectivityScope;
+        }
+
+        @Override
+        public int hashCode() {
+            TetheringRequestParcel parcel = getParcel();
+            return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
+                    parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
+                    parcel.showProvisioningUi, parcel.connectivityScope);
+        }
     }
 
     /**
@@ -1343,6 +1383,9 @@
     @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
     public void registerTetheringEventCallback(@NonNull Executor executor,
             @NonNull TetheringEventCallback callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "registerTetheringEventCallback caller:" + callerPkg);
 
@@ -1497,6 +1540,8 @@
             Manifest.permission.ACCESS_NETWORK_STATE
     })
     public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) {
+        Objects.requireNonNull(callback);
+
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "unregisterTetheringEventCallback caller:" + callerPkg);
 
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 544ba01..b807544 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -34,7 +34,6 @@
 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
 import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
-import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
 import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
 import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
 
@@ -45,10 +44,9 @@
 import android.net.LinkProperties;
 import android.net.MacAddress;
 import android.net.RouteInfo;
-import android.net.RoutingCoordinatorManager;
 import android.net.TetheredClient;
 import android.net.TetheringManager;
-import android.net.TetheringRequestParcel;
+import android.net.TetheringManager.TetheringRequest;
 import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
@@ -73,21 +71,17 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetdUtils;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorManager;
 import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
 import com.android.net.module.util.ip.InterfaceController;
-import com.android.net.module.util.ip.IpNeighborMonitor;
-import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
 import com.android.networkstack.tethering.BpfCoordinator;
-import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
 import com.android.networkstack.tethering.util.StateMachineShim;
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
 
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -137,8 +131,8 @@
     private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString("00:00:00:00:00:00");
 
     private static final String TAG = "IpServer";
-    private static final boolean DBG = false;
-    private static final boolean VDBG = false;
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
     private static final Class[] sMessageClasses = {
             IpServer.class
     };
@@ -189,12 +183,6 @@
             return new DadProxy(handler, ifParams);
         }
 
-        /** Create an IpNeighborMonitor to be used by this IpServer */
-        public IpNeighborMonitor getIpNeighborMonitor(Handler handler, SharedLog log,
-                IpNeighborMonitor.NeighborEventConsumer consumer) {
-            return new IpNeighborMonitor(handler, log, consumer);
-        }
-
         /** Create a RouterAdvertisementDaemon instance to be used by IpServer.*/
         public RouterAdvertisementDaemon getRouterAdvertisementDaemon(InterfaceParams ifParams) {
             return new RouterAdvertisementDaemon(ifParams);
@@ -234,13 +222,11 @@
     public static final int CMD_TETHER_CONNECTION_CHANGED   = BASE_IPSERVER + 9;
     // new IPv6 tethering parameters need to be processed
     public static final int CMD_IPV6_TETHER_UPDATE          = BASE_IPSERVER + 10;
-    // new neighbor cache entry on our interface
-    public static final int CMD_NEIGHBOR_EVENT              = BASE_IPSERVER + 11;
     // request from DHCP server that it wants to have a new prefix
-    public static final int CMD_NEW_PREFIX_REQUEST          = BASE_IPSERVER + 12;
+    public static final int CMD_NEW_PREFIX_REQUEST          = BASE_IPSERVER + 11;
     // request from PrivateAddressCoordinator to restart tethering.
-    public static final int CMD_NOTIFY_PREFIX_CONFLICT      = BASE_IPSERVER + 13;
-    public static final int CMD_SERVICE_FAILED_TO_START     = BASE_IPSERVER + 14;
+    public static final int CMD_NOTIFY_PREFIX_CONFLICT      = BASE_IPSERVER + 12;
+    public static final int CMD_SERVICE_FAILED_TO_START     = BASE_IPSERVER + 13;
 
     private final State mInitialState;
     private final State mLocalHotspotState;
@@ -252,11 +238,8 @@
     private final INetd mNetd;
     @NonNull
     private final BpfCoordinator mBpfCoordinator;
-    // Contains null if the connectivity module is unsupported, as the routing coordinator is not
-    // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
-    // must be able to find all classes at runtime.
     @NonNull
-    private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
+    private final RoutingCoordinatorManager mRoutingCoordinator;
     private final Callback mCallback;
     private final InterfaceController mInterfaceCtrl;
     private final PrivateAddressCoordinator mPrivateAddressCoordinator;
@@ -301,18 +284,9 @@
     private List<TetheredClient> mDhcpLeases = Collections.emptyList();
 
     private int mLastIPv6UpstreamIfindex = 0;
-    private boolean mUpstreamSupportsBpf = false;
     @NonNull
     private Set<IpPrefix> mLastIPv6UpstreamPrefixes = Collections.emptySet();
 
-    private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
-        public void accept(NeighborEvent e) {
-            sendMessage(CMD_NEIGHBOR_EVENT, e);
-        }
-    }
-
-    private final IpNeighborMonitor mIpNeighborMonitor;
-
     private LinkAddress mIpv4Address;
 
     private final TetheringMetrics mTetheringMetrics;
@@ -323,7 +297,7 @@
     public IpServer(
             String ifaceName, Handler handler, int interfaceType, SharedLog log,
             INetd netd, @NonNull BpfCoordinator bpfCoordinator,
-            @Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
+            RoutingCoordinatorManager routingCoordinatorManager, Callback callback,
             TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
             TetheringMetrics tetheringMetrics, Dependencies deps) {
         super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
@@ -331,7 +305,7 @@
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
         mBpfCoordinator = bpfCoordinator;
-        mRoutingCoordinator = routingCoordinator;
+        mRoutingCoordinator = routingCoordinatorManager;
         mCallback = callback;
         mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
         mIfaceName = ifaceName;
@@ -346,15 +320,6 @@
         mLastError = TETHER_ERROR_NO_ERROR;
         mServingMode = STATE_AVAILABLE;
 
-        mIpNeighborMonitor = mDeps.getIpNeighborMonitor(getHandler(), mLog,
-                new MyNeighborEventConsumer());
-
-        // IP neighbor monitor monitors the neighbor events for adding/removing IPv6 downstream rule
-        // per client. If BPF offload is not supported, don't start listening for neighbor events.
-        if (mBpfCoordinator.isUsingBpfOffload() && !mIpNeighborMonitor.start()) {
-            mLog.e("Failed to create IpNeighborMonitor on " + mIfaceName);
-        }
-
         mInitialState = new InitialState();
         mLocalHotspotState = new LocalHotspotState();
         mTetheredState = new TetheredState();
@@ -410,6 +375,22 @@
         return mIpv4Address;
     }
 
+    /** The IPv6 upstream interface index */
+    public int getIpv6UpstreamIfindex() {
+        return mLastIPv6UpstreamIfindex;
+    }
+
+    /** The IPv6 upstream interface prefixes */
+    @NonNull
+    public Set<IpPrefix> getIpv6UpstreamPrefixes() {
+        return Collections.unmodifiableSet(mLastIPv6UpstreamPrefixes);
+    }
+
+    /** The interface parameters which IpServer is using */
+    public InterfaceParams getInterfaceParams() {
+        return mInterfaceParams;
+    }
+
     /**
      * Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper
      * thread.
@@ -419,7 +400,7 @@
     }
 
     /** Enable this IpServer. IpServer state machine will be tethered or localHotspot state. */
-    public void enable(final int requestedState, final TetheringRequestParcel request) {
+    public void enable(final int requestedState, final TetheringRequest request) {
         sendMessage(CMD_TETHER_REQUESTED, requestedState, 0, request);
     }
 
@@ -813,14 +794,15 @@
         setRaParams(params);
 
         // Not support BPF on virtual upstream interface
-        final boolean upstreamSupportsBpf = upstreamIface != null && !isVcnInterface(upstreamIface);
         final Set<IpPrefix> upstreamPrefixes = params != null ? params.prefixes : Set.of();
-        updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamPrefixes,
-                upstreamIfIndex, upstreamPrefixes, upstreamSupportsBpf);
+        // mBpfCoordinator#updateIpv6UpstreamInterface must be called before updating
+        // mLastIPv6UpstreamIfindex and mLastIPv6UpstreamPrefixes because BpfCoordinator will call
+        // IpServer#getIpv6UpstreamIfindex and IpServer#getIpv6UpstreamPrefixes to retrieve current
+        // upstream interface index and prefixes when handling upstream changes.
+        mBpfCoordinator.updateIpv6UpstreamInterface(this, upstreamIfIndex, upstreamPrefixes);
         mLastIPv6LinkProperties = v6only;
         mLastIPv6UpstreamIfindex = upstreamIfIndex;
         mLastIPv6UpstreamPrefixes = upstreamPrefixes;
-        mUpstreamSupportsBpf = upstreamSupportsBpf;
         if (mDadProxy != null) {
             mDadProxy.setUpstreamIface(upstreamIfaceParams);
         }
@@ -839,47 +821,25 @@
 
     private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
         try {
-            if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-                // TODO : remove this call in favor of using the LocalNetworkConfiguration
-                // correctly, which will let ConnectivityService do it automatically.
-                mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
-            } else {
-                mNetd.networkAddInterface(netId, ifaceName);
-            }
-        } catch (ServiceSpecificException | RemoteException e) {
+            // TODO : remove this call in favor of using the LocalNetworkConfiguration
+            // correctly, which will let ConnectivityService do it automatically.
+            mRoutingCoordinator.addInterfaceToNetwork(netId, ifaceName);
+        } catch (ServiceSpecificException e) {
             mLog.e("Failed to add " + mIfaceName + " to local table: ", e);
         }
     }
 
-    private void addInterfaceForward(@NonNull final String fromIface,
-            @NonNull final String toIface) throws ServiceSpecificException, RemoteException {
-        if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-            mRoutingCoordinator.value.addInterfaceForward(fromIface, toIface);
-        } else {
-            mNetd.tetherAddForward(fromIface, toIface);
-            mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
-        }
+    private void addInterfaceForward(@NonNull final String fromIface, @NonNull final String toIface)
+            throws ServiceSpecificException {
+        mRoutingCoordinator.addInterfaceForward(fromIface, toIface);
     }
 
     private void removeInterfaceForward(@NonNull final String fromIface,
             @NonNull final String toIface) {
-        if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-            try {
-                mRoutingCoordinator.value.removeInterfaceForward(fromIface, toIface);
-            } catch (ServiceSpecificException e) {
-                mLog.e("Exception in removeInterfaceForward", e);
-            }
-        } else {
-            try {
-                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
-            } catch (RemoteException | ServiceSpecificException e) {
-                mLog.e("Exception in ipfwdRemoveInterfaceForward", e);
-            }
-            try {
-                mNetd.tetherRemoveForward(fromIface, toIface);
-            } catch (RemoteException | ServiceSpecificException e) {
-                mLog.e("Exception in disableNat", e);
-            }
+        try {
+            mRoutingCoordinator.removeInterfaceForward(fromIface, toIface);
+        } catch (RuntimeException e) {
+            mLog.e("Exception in removeInterfaceForward", e);
         }
     }
 
@@ -964,77 +924,6 @@
         }
     }
 
-    private int getInterfaceIndexForRule(int ifindex, boolean supportsBpf) {
-        return supportsBpf ? ifindex : NO_UPSTREAM;
-    }
-
-    // Handles updates to IPv6 forwarding rules if the upstream or its prefixes change.
-    private void updateIpv6ForwardingRules(int prevUpstreamIfindex,
-            @NonNull Set<IpPrefix> prevUpstreamPrefixes, int upstreamIfindex,
-            @NonNull Set<IpPrefix> upstreamPrefixes, boolean upstreamSupportsBpf) {
-        // If the upstream interface has changed, remove all rules and re-add them with the new
-        // upstream interface. If upstream is a virtual network, treated as no upstream.
-        if (prevUpstreamIfindex != upstreamIfindex
-                || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
-            mBpfCoordinator.updateAllIpv6Rules(this, this.mInterfaceParams,
-                    getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
-                    upstreamPrefixes);
-        }
-    }
-
-    // Handles updates to IPv6 downstream rules if a neighbor event is received.
-    private void addOrRemoveIpv6Downstream(NeighborEvent e) {
-        // mInterfaceParams must be non-null or the event would not have arrived.
-        if (e == null) return;
-        if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress()
-                || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
-            return;
-        }
-
-        // When deleting rules, we still need to pass a non-null MAC, even though it's ignored.
-        // Do this here instead of in the Ipv6DownstreamRule constructor to ensure that we
-        // never add rules with a null MAC, only delete them.
-        MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
-        Ipv6DownstreamRule rule = new Ipv6DownstreamRule(
-                getInterfaceIndexForRule(mLastIPv6UpstreamIfindex, mUpstreamSupportsBpf),
-                mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
-        if (e.isValid()) {
-            mBpfCoordinator.addIpv6DownstreamRule(this, rule);
-        } else {
-            mBpfCoordinator.removeIpv6DownstreamRule(this, rule);
-        }
-    }
-
-    // TODO: consider moving into BpfCoordinator.
-    private void updateClientInfoIpv4(NeighborEvent e) {
-        if (e == null) return;
-        if (!(e.ip instanceof Inet4Address) || e.ip.isMulticastAddress()
-                || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
-            return;
-        }
-
-        // When deleting clients, IpServer still need to pass a non-null MAC, even though it's
-        // ignored. Do this here instead of in the ClientInfo constructor to ensure that
-        // IpServer never add clients with a null MAC, only delete them.
-        final MacAddress clientMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
-        final ClientInfo clientInfo = new ClientInfo(mInterfaceParams.index,
-                mInterfaceParams.macAddr, (Inet4Address) e.ip, clientMac);
-        if (e.isValid()) {
-            mBpfCoordinator.tetherOffloadClientAdd(this, clientInfo);
-        } else {
-            mBpfCoordinator.tetherOffloadClientRemove(this, clientInfo);
-        }
-    }
-
-    private void handleNeighborEvent(NeighborEvent e) {
-        if (mInterfaceParams != null
-                && mInterfaceParams.index == e.ifindex
-                && mInterfaceParams.hasMacAddress) {
-            addOrRemoveIpv6Downstream(e);
-            updateClientInfoIpv4(e);
-        }
-    }
-
     private byte getHopLimit(String upstreamIface, int adjustTTL) {
         try {
             int upstreamHopLimit = Integer.parseUnsignedInt(
@@ -1069,7 +958,6 @@
         switch (what) {
             // Suppress some CMD_* to avoid log flooding.
             case CMD_IPV6_TETHER_UPDATE:
-            case CMD_NEIGHBOR_EVENT:
                 break;
             default:
                 mLog.log(state.getName() + " got "
@@ -1092,18 +980,18 @@
         mLinkProperties.setInterfaceName(mIfaceName);
     }
 
-    private void maybeConfigureStaticIp(final TetheringRequestParcel request) {
+    private void maybeConfigureStaticIp(final TetheringRequest request) {
         // Ignore static address configuration if they are invalid or null. In theory, static
         // addresses should not be invalid here because TetheringManager do not allow caller to
         // specify invalid static address configuration.
-        if (request == null || request.localIPv4Address == null
-                || request.staticClientAddress == null || !checkStaticAddressConfiguration(
-                request.localIPv4Address, request.staticClientAddress)) {
+        if (request == null || request.getLocalIpv4Address() == null
+                || request.getClientStaticIpv4Address() == null || !checkStaticAddressConfiguration(
+                request.getLocalIpv4Address(), request.getClientStaticIpv4Address())) {
             return;
         }
 
-        mStaticIpv4ServerAddr = request.localIPv4Address;
-        mStaticIpv4ClientAddr = request.staticClientAddress;
+        mStaticIpv4ServerAddr = request.getLocalIpv4Address();
+        mStaticIpv4ClientAddr = request.getClientStaticIpv4Address();
     }
 
     class InitialState extends State {
@@ -1120,11 +1008,11 @@
                     mLastError = TETHER_ERROR_NO_ERROR;
                     switch (message.arg1) {
                         case STATE_LOCAL_ONLY:
-                            maybeConfigureStaticIp((TetheringRequestParcel) message.obj);
+                            maybeConfigureStaticIp((TetheringRequest) message.obj);
                             transitionTo(mLocalHotspotState);
                             break;
                         case STATE_TETHERED:
-                            maybeConfigureStaticIp((TetheringRequestParcel) message.obj);
+                            maybeConfigureStaticIp((TetheringRequest) message.obj);
                             transitionTo(mTetheredState);
                             break;
                         default:
@@ -1141,14 +1029,6 @@
         }
     }
 
-    private void startConntrackMonitoring() {
-        mBpfCoordinator.startMonitoring(this);
-    }
-
-    private void stopConntrackMonitoring() {
-        mBpfCoordinator.stopMonitoring(this);
-    }
-
     abstract class BaseServingState extends State {
         private final int mDesiredInterfaceState;
 
@@ -1158,7 +1038,7 @@
 
         @Override
         public void enter() {
-            startConntrackMonitoring();
+            mBpfCoordinator.addIpServer(IpServer.this);
 
             startServingInterface();
 
@@ -1226,7 +1106,7 @@
             }
 
             stopIPv4();
-            stopConntrackMonitoring();
+            mBpfCoordinator.removeIpServer(IpServer.this);
 
             resetLinkProperties();
 
@@ -1397,8 +1277,8 @@
 
             for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
             mUpstreamIfaceSet = null;
-            mBpfCoordinator.updateAllIpv6Rules(
-                    IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM, Set.of());
+            mBpfCoordinator.updateIpv6UpstreamInterface(IpServer.this, NO_UPSTREAM,
+                    Collections.emptySet());
         }
 
         private void cleanupUpstreamInterface(String upstreamIface) {
@@ -1464,7 +1344,7 @@
                         mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
                         try {
                             addInterfaceForward(mIfaceName, ifname);
-                        } catch (RemoteException | ServiceSpecificException e) {
+                        } catch (RuntimeException e) {
                             mLog.e("Exception enabling iface forward", e);
                             cleanupUpstream();
                             mLastError = TETHER_ERROR_ENABLE_FORWARDING_ERROR;
@@ -1473,9 +1353,6 @@
                         }
                     }
                     break;
-                case CMD_NEIGHBOR_EVENT:
-                    handleNeighborEvent((NeighborEvent) message.obj);
-                    break;
                 default:
                     return false;
             }
@@ -1515,9 +1392,6 @@
     class UnavailableState extends State {
         @Override
         public void enter() {
-            // TODO: move mIpNeighborMonitor.stop() to TetheredState#exit, and trigger a neighbours
-            //       dump after starting mIpNeighborMonitor.
-            mIpNeighborMonitor.stop();
             mLastError = TETHER_ERROR_NO_ERROR;
             sendInterfaceState(STATE_UNAVAILABLE);
         }
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 81e18ab..5c853f4 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -69,7 +69,6 @@
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
@@ -77,6 +76,9 @@
 import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.ip.ConntrackMonitor;
 import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEventConsumer;
+import com.android.net.module.util.ip.IpNeighborMonitor;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
 import com.android.net.module.util.netlink.ConntrackMessage;
 import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkUtils;
@@ -182,6 +184,10 @@
     private final BpfCoordinatorShim mBpfCoordinatorShim;
     @NonNull
     private final BpfConntrackEventConsumer mBpfConntrackEventConsumer;
+    @NonNull
+    private final IpNeighborMonitor mIpNeighborMonitor;
+    @NonNull
+    private final BpfNeighborEventConsumer mBpfNeighborEventConsumer;
 
     // True if BPF offload is supported, false otherwise. The BPF offload could be disabled by
     // a runtime resource overlay package or device configuration. This flag is only initialized
@@ -190,14 +196,6 @@
     // to make it simpler. See also TetheringConfiguration.
     private final boolean mIsBpfEnabled;
 
-    // Tracks whether BPF tethering is started or not. This is set by tethering before it
-    // starts the first IpServer and is cleared by tethering shortly before the last IpServer
-    // is stopped. Note that rule updates (especially deletions, but sometimes additions as
-    // well) may arrive when this is false. If they do, they must be communicated to netd.
-    // Changes in data limits may also arrive when this is false, and if they do, they must
-    // also be communicated to netd.
-    private boolean mPollingStarted = false;
-
     // Tracking remaining alert quota. Unlike limit quota is subject to interface, the alert
     // quota is interface independent and global for tether offload.
     private long mRemainingAlertQuota = QUOTA_UNLIMITED;
@@ -280,9 +278,6 @@
     private final HashMap<IpServer, HashMap<Inet4Address, ClientInfo>>
             mTetherClients = new HashMap<>();
 
-    // Set for which downstream is monitoring the conntrack netlink message.
-    private final Set<IpServer> mMonitoringIpServers = new HashSet<>();
-
     // Map of upstream interface IPv4 address to interface index.
     // TODO: consider making the key to be unique because the upstream address is not unique. It
     // is okay for now because there have only one upstream generally.
@@ -304,16 +299,19 @@
     @Nullable
     private UpstreamInfo mIpv4UpstreamInfo = null;
 
+    // The IpServers that are currently served by BpfCoordinator.
+    private final ArraySet<IpServer> mServedIpServers = new ArraySet<>();
+
     // Runnable that used by scheduling next polling of stats.
     private final Runnable mScheduledPollingStats = () -> {
         updateForwardedStats();
-        maybeSchedulePollingStats();
+        schedulePollingStats();
     };
 
     // Runnable that used by scheduling next refreshing of conntrack timeout.
     private final Runnable mScheduledConntrackTimeoutUpdate = () -> {
         refreshAllConntrackTimeouts();
-        maybeScheduleConntrackTimeoutUpdate();
+        scheduleConntrackTimeoutUpdate();
     };
 
     // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
@@ -339,6 +337,11 @@
             return new ConntrackMonitor(getHandler(), getSharedLog(), consumer);
         }
 
+        /** Get ip neighbor monitor */
+        @NonNull public IpNeighborMonitor getIpNeighborMonitor(NeighborEventConsumer consumer) {
+            return new IpNeighborMonitor(getHandler(), getSharedLog(), consumer);
+        }
+
         /** Get interface information for a given interface. */
         @NonNull public InterfaceParams getInterfaceParams(String ifName) {
             return InterfaceParams.getByName(ifName);
@@ -486,6 +489,9 @@
         mBpfConntrackEventConsumer = new BpfConntrackEventConsumer();
         mConntrackMonitor = mDeps.getConntrackMonitor(mBpfConntrackEventConsumer);
 
+        mBpfNeighborEventConsumer = new BpfNeighborEventConsumer();
+        mIpNeighborMonitor = mDeps.getIpNeighborMonitor(mBpfNeighborEventConsumer);
+
         BpfTetherStatsProvider provider = new BpfTetherStatsProvider();
         try {
             mDeps.getNetworkStatsManager().registerNetworkStatsProvider(
@@ -505,37 +511,25 @@
     }
 
     /**
-     * Start BPF tethering offload stats polling when the first upstream is started.
+     * Start BPF tethering offload stats and conntrack timeout polling.
      * Note that this can be only called on handler thread.
-     * TODO: Perhaps check BPF support before starting.
-     * TODO: Start the stats polling only if there is any client on the downstream.
      */
-    public void startPolling() {
-        if (mPollingStarted) return;
+    private void startStatsAndConntrackTimeoutPolling() {
+        schedulePollingStats();
+        scheduleConntrackTimeoutUpdate();
 
-        if (!isUsingBpf()) {
-            mLog.i("BPF is not using");
-            return;
-        }
-
-        mPollingStarted = true;
-        maybeSchedulePollingStats();
-        maybeScheduleConntrackTimeoutUpdate();
-
-        mLog.i("Polling started");
+        mLog.i("Polling started.");
     }
 
     /**
-     * Stop BPF tethering offload stats polling.
+     * Stop BPF tethering offload stats and conntrack timeout polling.
      * The data limit cleanup and the tether stats maps cleanup are not implemented here.
      * These cleanups rely on all IpServers calling #removeIpv6DownstreamRule. After the
      * last rule is removed from the upstream, #removeIpv6DownstreamRule does the cleanup
      * functionality.
      * Note that this can be only called on handler thread.
      */
-    public void stopPolling() {
-        if (!mPollingStarted) return;
-
+    private void stopStatsAndConntrackTimeoutPolling() {
         // Stop scheduled polling conntrack timeout.
         if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
             mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
@@ -545,9 +539,8 @@
             mHandler.removeCallbacks(mScheduledPollingStats);
         }
         updateForwardedStats();
-        mPollingStarted = false;
 
-        mLog.i("Polling stopped");
+        mLog.i("Polling stopped.");
     }
 
     /**
@@ -568,7 +561,6 @@
 
     /**
      * Start conntrack message monitoring.
-     * Note that this can be only called on handler thread.
      *
      * TODO: figure out a better logging for non-interesting conntrack message.
      * For example, the following logging is an IPCTNL_MSG_CT_GET message but looks scary.
@@ -588,45 +580,23 @@
      * +------------------+--------------------------------------------------------+
      * See NetlinkMonitor#handlePacket, NetlinkMessage#parseNfMessage.
      */
-    public void startMonitoring(@NonNull final IpServer ipServer) {
+    private void startConntrackMonitoring() {
         // TODO: Wrap conntrackMonitor starting function into mBpfCoordinatorShim.
-        if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
+        if (!mDeps.isAtLeastS()) return;
 
-        if (mMonitoringIpServers.contains(ipServer)) {
-            Log.wtf(TAG, "The same downstream " + ipServer.interfaceName()
-                    + " should not start monitoring twice.");
-            return;
-        }
-
-        if (mMonitoringIpServers.isEmpty()) {
-            mConntrackMonitor.start();
-            mLog.i("Monitoring started");
-        }
-
-        mMonitoringIpServers.add(ipServer);
+        mConntrackMonitor.start();
+        mLog.i("Conntrack monitoring started.");
     }
 
     /**
      * Stop conntrack event monitoring.
-     * Note that this can be only called on handler thread.
      */
-    public void stopMonitoring(@NonNull final IpServer ipServer) {
+    private void stopConntrackMonitoring() {
         // TODO: Wrap conntrackMonitor stopping function into mBpfCoordinatorShim.
-        if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
-
-        // Ignore stopping monitoring if the monitor has never started for a given IpServer.
-        if (!mMonitoringIpServers.contains(ipServer)) {
-            mLog.e("Ignore stopping monitoring because monitoring has never started for "
-                    + ipServer.interfaceName());
-            return;
-        }
-
-        mMonitoringIpServers.remove(ipServer);
-
-        if (!mMonitoringIpServers.isEmpty()) return;
+        if (!mDeps.isAtLeastS()) return;
 
         mConntrackMonitor.stop();
-        mLog.i("Monitoring stopped");
+        mLog.i("Conntrack monitoring stopped.");
     }
 
     /**
@@ -689,9 +659,8 @@
 
     /**
      * Add IPv6 downstream rule.
-     * Note that this can be only called on handler thread.
      */
-    public void addIpv6DownstreamRule(
+    private void addIpv6DownstreamRule(
             @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
         if (!isUsingBpf()) return;
 
@@ -707,9 +676,8 @@
 
     /**
      * Remove IPv6 downstream rule.
-     * Note that this can be only called on handler thread.
      */
-    public void removeIpv6DownstreamRule(
+    private void removeIpv6DownstreamRule(
             @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
         if (!isUsingBpf()) return;
 
@@ -763,9 +731,8 @@
     /**
      * Delete all upstream and downstream rules for the passed-in IpServer, and if the new upstream
      * is nonzero, reapply them to the new upstream.
-     * Note that this can be only called on handler thread.
      */
-    public void updateAllIpv6Rules(@NonNull final IpServer ipServer,
+    private void updateAllIpv6Rules(@NonNull final IpServer ipServer,
             final InterfaceParams interfaceParams, int newUpstreamIfindex,
             @NonNull final Set<IpPrefix> newUpstreamPrefixes) {
         if (!isUsingBpf()) return;
@@ -887,6 +854,141 @@
     }
 
     /**
+     * Register an IpServer (downstream).
+     * Note that this can be only called on handler thread.
+     */
+    public void addIpServer(@NonNull final IpServer ipServer) {
+        if (!isUsingBpf()) return;
+        if (mServedIpServers.contains(ipServer)) {
+            Log.wtf(TAG, "The same downstream " + ipServer.interfaceName()
+                    + " should not add twice.");
+            return;
+        }
+
+        // Start monitoring and polling when the first IpServer is added.
+        if (mServedIpServers.isEmpty()) {
+            startStatsAndConntrackTimeoutPolling();
+            startConntrackMonitoring();
+            mIpNeighborMonitor.start();
+            mLog.i("Neighbor monitoring started.");
+        }
+        mServedIpServers.add(ipServer);
+    }
+
+    /**
+     * Unregister an IpServer (downstream).
+     * Note that this can be only called on handler thread.
+     */
+    public void removeIpServer(@NonNull final IpServer ipServer) {
+        if (!isUsingBpf()) return;
+        if (!mServedIpServers.contains(ipServer)) {
+            mLog.e("Ignore removing because IpServer has never started for "
+                    + ipServer.interfaceName());
+            return;
+        }
+        mServedIpServers.remove(ipServer);
+
+        // Stop monitoring and polling when the last IpServer is removed.
+        if (mServedIpServers.isEmpty()) {
+            stopStatsAndConntrackTimeoutPolling();
+            stopConntrackMonitoring();
+            mIpNeighborMonitor.stop();
+            mLog.i("Neighbor monitoring stopped.");
+        }
+    }
+
+    /**
+     * Update upstream interface and its prefixes.
+     * Note that this can be only called on handler thread.
+     */
+    public void updateIpv6UpstreamInterface(@NonNull final IpServer ipServer, int upstreamIfindex,
+            @NonNull Set<IpPrefix> upstreamPrefixes) {
+        if (!isUsingBpf()) return;
+
+        // If the upstream interface has changed, remove all rules and re-add them with the new
+        // upstream interface. If upstream is a virtual network, treated as no upstream.
+        final int prevUpstreamIfindex = ipServer.getIpv6UpstreamIfindex();
+        final InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+        final Set<IpPrefix> prevUpstreamPrefixes = ipServer.getIpv6UpstreamPrefixes();
+        if (prevUpstreamIfindex != upstreamIfindex
+                || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
+            final boolean upstreamSupportsBpf = checkUpstreamSupportsBpf(upstreamIfindex);
+            updateAllIpv6Rules(ipServer, interfaceParams,
+                    getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
+                    upstreamPrefixes);
+        }
+    }
+
+    private boolean checkUpstreamSupportsBpf(int upstreamIfindex) {
+        final String iface = mInterfaceNames.get(upstreamIfindex);
+        return iface != null && !isVcnInterface(iface);
+    }
+
+    private int getInterfaceIndexForRule(int ifindex, boolean supportsBpf) {
+        return supportsBpf ? ifindex : NO_UPSTREAM;
+    }
+
+    // Handles updates to IPv6 downstream rules if a neighbor event is received.
+    private void addOrRemoveIpv6Downstream(@NonNull IpServer ipServer, NeighborEvent e) {
+        // mInterfaceParams must be non-null or the event would not have arrived.
+        if (e == null) return;
+        if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress()
+                || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
+            return;
+        }
+
+        // When deleting rules, we still need to pass a non-null MAC, even though it's ignored.
+        // Do this here instead of in the Ipv6DownstreamRule constructor to ensure that we
+        // never add rules with a null MAC, only delete them.
+        final InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+        if (interfaceParams == null || interfaceParams.macAddr == null) return;
+        final int lastIpv6UpstreamIfindex = ipServer.getIpv6UpstreamIfindex();
+        final boolean isUpstreamSupportsBpf = checkUpstreamSupportsBpf(lastIpv6UpstreamIfindex);
+        MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
+        Ipv6DownstreamRule rule = new Ipv6DownstreamRule(
+                getInterfaceIndexForRule(lastIpv6UpstreamIfindex, isUpstreamSupportsBpf),
+                interfaceParams.index, (Inet6Address) e.ip, interfaceParams.macAddr, dstMac);
+        if (e.isValid()) {
+            addIpv6DownstreamRule(ipServer, rule);
+        } else {
+            removeIpv6DownstreamRule(ipServer, rule);
+        }
+    }
+
+    private void updateClientInfoIpv4(@NonNull IpServer ipServer, NeighborEvent e) {
+        if (e == null) return;
+        if (!(e.ip instanceof Inet4Address) || e.ip.isMulticastAddress()
+                || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
+            return;
+        }
+
+        InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+        if (interfaceParams == null) return;
+
+        // When deleting clients, IpServer still need to pass a non-null MAC, even though it's
+        // ignored. Do this here instead of in the ClientInfo constructor to ensure that
+        // IpServer never add clients with a null MAC, only delete them.
+        final MacAddress clientMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
+        final ClientInfo clientInfo = new ClientInfo(interfaceParams.index,
+                interfaceParams.macAddr, (Inet4Address) e.ip, clientMac);
+        if (e.isValid()) {
+            tetherOffloadClientAdd(ipServer, clientInfo);
+        } else {
+            tetherOffloadClientRemove(ipServer, clientInfo);
+        }
+    }
+
+    private void handleNeighborEvent(@NonNull IpServer ipServer, NeighborEvent e) {
+        InterfaceParams interfaceParams = ipServer.getInterfaceParams();
+        if (interfaceParams != null
+                && interfaceParams.index == e.ifindex
+                && interfaceParams.hasMacAddress) {
+            addOrRemoveIpv6Downstream(ipServer, e);
+            updateClientInfoIpv4(ipServer, e);
+        }
+    }
+
+    /**
      * Clear all forwarding IPv4 rules for a given client.
      * Note that this can be only called on handler thread.
      */
@@ -1137,7 +1239,7 @@
         // Note that EthernetTetheringTest#isTetherConfigBpfOffloadEnabled relies on
         // "mIsBpfEnabled" to check tethering config via dumpsys. Beware of the change if any.
         pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
-        pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
+        pw.println("Polling " + (mServedIpServers.isEmpty() ? "not started" : "started"));
         pw.println("Stats provider " + (mStatsProvider != null
                 ? "registered" : "not registered"));
         pw.println("Upstream quota: " + mInterfaceQuotas.toString());
@@ -1343,19 +1445,6 @@
         pw.decreaseIndent();
     }
 
-    private <K extends Struct, V extends Struct> void dumpRawMap(IBpfMap<K, V> map,
-            IndentingPrintWriter pw) throws ErrnoException {
-        if (map == null) {
-            pw.println("No BPF support");
-            return;
-        }
-        if (map.isEmpty()) {
-            pw.println("No entries");
-            return;
-        }
-        map.forEach((k, v) -> pw.println(BpfDump.toBase64EncodedString(k, v)));
-    }
-
     /**
      * Dump raw BPF map into the base64 encoded strings "<base64 key>,<base64 value>".
      * Allow to dump only one map path once. For test only.
@@ -1375,16 +1464,16 @@
         // TODO: dump downstream4 map.
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
             try (IBpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
-                dumpRawMap(statsMap, pw);
-            } catch (ErrnoException | IOException e) {
+                BpfDump.dumpRawMap(statsMap, pw);
+            } catch (IOException e) {
                 pw.println("Error dumping stats map: " + e);
             }
             return;
         }
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
             try (IBpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
-                dumpRawMap(upstreamMap, pw);
-            } catch (ErrnoException | IOException e) {
+                BpfDump.dumpRawMap(upstreamMap, pw);
+            } catch (IOException e) {
                 pw.println("Error dumping IPv4 map: " + e);
             }
             return;
@@ -2052,6 +2141,15 @@
         }
     }
 
+    @VisibleForTesting
+    private class BpfNeighborEventConsumer implements NeighborEventConsumer {
+        public void accept(NeighborEvent e) {
+            for (IpServer ipServer : mServedIpServers) {
+                handleNeighborEvent(ipServer, e);
+            }
+        }
+    }
+
     private boolean isBpfEnabled() {
         final TetheringConfiguration config = mDeps.getTetherConfig();
         return (config != null) ? config.isBpfOffloadEnabled() : true /* default value */;
@@ -2379,9 +2477,7 @@
         });
     }
 
-    private void maybeSchedulePollingStats() {
-        if (!mPollingStarted) return;
-
+    private void schedulePollingStats() {
         if (mHandler.hasCallbacks(mScheduledPollingStats)) {
             mHandler.removeCallbacks(mScheduledPollingStats);
         }
@@ -2389,9 +2485,7 @@
         mHandler.postDelayed(mScheduledPollingStats, getPollingInterval());
     }
 
-    private void maybeScheduleConntrackTimeoutUpdate() {
-        if (!mPollingStarted) return;
-
+    private void scheduleConntrackTimeoutUpdate() {
         if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
             mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
         }
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 873961a..d62f18f 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -38,6 +38,7 @@
 import static android.net.TetheringManager.TETHERING_INVALID;
 import static android.net.TetheringManager.TETHERING_NCM;
 import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHERING_WIGIG;
@@ -90,14 +91,12 @@
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkInfo;
-import android.net.RoutingCoordinatorManager;
 import android.net.TetherStatesParcel;
 import android.net.TetheredClient;
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
 import android.net.TetheringInterface;
 import android.net.TetheringManager.TetheringRequest;
-import android.net.TetheringRequestParcel;
 import android.net.Uri;
 import android.net.ip.IpServer;
 import android.net.wifi.WifiClient;
@@ -138,7 +137,7 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.NetdUtils;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorManager;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
@@ -147,7 +146,6 @@
 import com.android.networkstack.tethering.metrics.TetheringMetrics;
 import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.networkstack.tethering.util.PrefixUtils;
-import com.android.networkstack.tethering.util.TetheringUtils;
 import com.android.networkstack.tethering.util.VersionedBroadcastListener;
 import com.android.networkstack.tethering.wear.WearableConnectionManager;
 
@@ -173,8 +171,8 @@
 public class Tethering {
 
     private static final String TAG = Tethering.class.getSimpleName();
-    private static final boolean DBG = false;
-    private static final boolean VDBG = false;
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
 
     private static final Class[] sMessageClasses = {
             Tethering.class, TetherMainSM.class, IpServer.class
@@ -231,7 +229,7 @@
     // Currently active tethering requests per tethering type. Only one of each type can be
     // requested at a time. After a tethering type is requested, the map keeps tethering parameters
     // to be used after the interface comes up asynchronously.
-    private final SparseArray<TetheringRequestParcel> mActiveTetheringRequests =
+    private final SparseArray<TetheringRequest> mActiveTetheringRequests =
             new SparseArray<>();
 
     private final Context mContext;
@@ -241,19 +239,13 @@
     private final TetherMainSM mTetherMainSM;
     private final OffloadController mOffloadController;
     private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
-    // TODO: Figure out how to merge this and other downstream-tracking objects
-    // into a single coherent structure.
-    private final HashSet<IpServer> mForwardedDownstreams;
     private final VersionedBroadcastListener mCarrierConfigChange;
     private final TetheringDependencies mDeps;
     private final EntitlementManager mEntitlementMgr;
     private final Handler mHandler;
     private final INetd mNetd;
     private final NetdCallback mNetdCallback;
-    // Contains null if the connectivity module is unsupported, as the routing coordinator is not
-    // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
-    // must be able to find all classes at runtime.
-    @NonNull private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
+    private final RoutingCoordinatorManager mRoutingCoordinator;
     private final UserRestrictionActionListener mTetheringRestriction;
     private final ActiveDataSubIdListener mActiveDataSubIdListener;
     private final ConnectedClientsTracker mConnectedClientsTracker;
@@ -271,8 +263,6 @@
 
     private boolean mRndisEnabled;       // track the RNDIS function enabled state
     private boolean mNcmEnabled;         // track the NCM function enabled state
-    // True iff. WiFi tethering should be started when soft AP is ready.
-    private boolean mWifiTetherRequested;
     private Network mTetherUpstream;
     private TetherStatesParcel mTetherStatesParcel;
     private boolean mDataSaverEnabled = false;
@@ -283,6 +273,7 @@
     private TetheredInterfaceRequestShim mBluetoothIfaceRequest;
     private String mConfiguredEthernetIface;
     private String mConfiguredBluetoothIface;
+    private String mConfiguredVirtualIface;
     private EthernetCallback mEthernetCallback;
     private TetheredInterfaceCallbackShim mBluetoothCallback;
     private SettingsObserver mSettingsObserver;
@@ -299,11 +290,11 @@
         mLog.mark("Tethering.constructed");
         mDeps = deps;
         mContext = mDeps.getContext();
-        mNetd = mDeps.getINetd(mContext);
-        mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext);
+        mNetd = mDeps.getINetd(mContext, mLog);
+        mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext, mLog);
         mLooper = mDeps.makeTetheringLooper();
         mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
-        mTetheringMetrics = mDeps.makeTetheringMetrics();
+        mTetheringMetrics = mDeps.makeTetheringMetrics(mContext);
 
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -329,7 +320,6 @@
                 (what, obj) -> {
                     mTetherMainSM.sendMessage(TetherMainSM.EVENT_UPSTREAM_CALLBACK, what, 0, obj);
                 });
-        mForwardedDownstreams = new HashSet<>();
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
@@ -665,28 +655,27 @@
         processInterfaceStateChange(iface, false /* enabled */);
     }
 
-    void startTethering(final TetheringRequestParcel request, final String callerPkg,
+    void startTethering(final TetheringRequest request, final String callerPkg,
             final IIntResultListener listener) {
         mHandler.post(() -> {
-            final TetheringRequestParcel unfinishedRequest = mActiveTetheringRequests.get(
-                    request.tetheringType);
+            final int type = request.getTetheringType();
+            final TetheringRequest unfinishedRequest = mActiveTetheringRequests.get(type);
             // If tethering is already enabled with a different request,
             // disable before re-enabling.
-            if (unfinishedRequest != null
-                    && !TetheringUtils.isTetheringRequestEquals(unfinishedRequest, request)) {
-                enableTetheringInternal(request.tetheringType, false /* disabled */, null);
-                mEntitlementMgr.stopProvisioningIfNeeded(request.tetheringType);
+            if (unfinishedRequest != null && !unfinishedRequest.equals(request)) {
+                enableTetheringInternal(type, false /* disabled */, null);
+                mEntitlementMgr.stopProvisioningIfNeeded(type);
             }
-            mActiveTetheringRequests.put(request.tetheringType, request);
+            mActiveTetheringRequests.put(type, request);
 
-            if (request.exemptFromEntitlementCheck) {
-                mEntitlementMgr.setExemptedDownstreamType(request.tetheringType);
+            if (request.isExemptFromEntitlementCheck()) {
+                mEntitlementMgr.setExemptedDownstreamType(type);
             } else {
-                mEntitlementMgr.startProvisioningIfNeeded(request.tetheringType,
-                        request.showProvisioningUi);
+                mEntitlementMgr.startProvisioningIfNeeded(type,
+                        request.getShouldShowEntitlementUi());
             }
-            enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
-            mTetheringMetrics.createBuilder(request.tetheringType, callerPkg);
+            enableTetheringInternal(type, true /* enabled */, listener);
+            mTetheringMetrics.createBuilder(type, callerPkg);
         });
     }
 
@@ -725,6 +714,9 @@
             case TETHERING_ETHERNET:
                 result = setEthernetTethering(enable);
                 break;
+            case TETHERING_VIRTUAL:
+                result = setVirtualMachineTethering(enable);
+                break;
             default:
                 Log.w(TAG, "Invalid tether type.");
                 result = TETHER_ERROR_UNKNOWN_TYPE;
@@ -763,7 +755,6 @@
             }
             if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */))
                     || (!enable && mgr.stopSoftAp())) {
-                mWifiTetherRequested = enable;
                 return TETHER_ERROR_NO_ERROR;
             }
         } finally {
@@ -979,6 +970,21 @@
         }
     }
 
+    private int setVirtualMachineTethering(final boolean enable) {
+        // TODO(340377643): Use bridge ifname when it's introduced, not fixed TAP ifname.
+        if (enable) {
+            mConfiguredVirtualIface = "avf_tap_fixed";
+            enableIpServing(
+                    TETHERING_VIRTUAL,
+                    mConfiguredVirtualIface,
+                    getRequestedState(TETHERING_VIRTUAL));
+        } else if (mConfiguredVirtualIface != null) {
+            ensureIpServerStopped(mConfiguredVirtualIface);
+            mConfiguredVirtualIface = null;
+        }
+        return TETHER_ERROR_NO_ERROR;
+    }
+
     void tether(String iface, int requestedState, final IIntResultListener listener) {
         mHandler.post(() -> {
             try {
@@ -1005,7 +1011,7 @@
         //
         // This code cannot race with untether() because they both run on the handler thread.
         final int type = tetherState.ipServer.interfaceType();
-        final TetheringRequestParcel request = mActiveTetheringRequests.get(type, null);
+        final TetheringRequest request = mActiveTetheringRequests.get(type, null);
         if (request != null) {
             mActiveTetheringRequests.delete(type);
         }
@@ -1062,14 +1068,14 @@
     }
 
     private int getRequestedState(int type) {
-        final TetheringRequestParcel request = mActiveTetheringRequests.get(type);
+        final TetheringRequest request = mActiveTetheringRequests.get(type);
 
         // The request could have been deleted before we had a chance to complete it.
         // If so, assume that the scope is the default scope for this tethering type.
         // This likely doesn't matter - if the request has been deleted, then tethering is
         // likely going to be stopped soon anyway.
         final int connectivityScope = (request != null)
-                ? request.connectivityScope
+                ? request.getConnectivityScope()
                 : TetheringRequest.getDefaultConnectivityScope(type);
 
         return connectivityScope == CONNECTIVITY_SCOPE_LOCAL
@@ -1368,7 +1374,7 @@
     }
 
     @VisibleForTesting
-    SparseArray<TetheringRequestParcel> getActiveTetheringRequests() {
+    SparseArray<TetheringRequest> getActiveTetheringRequests() {
         return mActiveTetheringRequests;
     }
 
@@ -1470,10 +1476,6 @@
     }
 
     private void disableWifiIpServing(String ifname, int apState) {
-        // Regardless of whether we requested this transition, the AP has gone
-        // down.  Don't try to tether again unless we're requested to do so.
-        mWifiTetherRequested = false;
-
         mLog.log("Canceling WiFi tethering request - interface=" + ifname + " state=" + apState);
 
         disableWifiIpServingCommon(TETHERING_WIFI, ifname);
@@ -1505,8 +1507,7 @@
     private void enableWifiIpServing(String ifname, int wifiIpMode) {
         mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
 
-        // Map wifiIpMode values to IpServer.Callback serving states, inferring
-        // from mWifiTetherRequested as a final "best guess".
+        // Map wifiIpMode values to IpServer.Callback serving states.
         final int ipServingMode;
         switch (wifiIpMode) {
             case IFACE_IP_MODE_TETHERED:
@@ -1653,11 +1654,6 @@
         mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
     }
 
-    private boolean upstreamWanted() {
-        if (!mForwardedDownstreams.isEmpty()) return true;
-        return mWifiTetherRequested;
-    }
-
     // Needed because the canonical source of upstream truth is just the
     // upstream interface set, |mCurrentUpstreamIfaceSet|.
     private boolean pertainsToCurrentUpstream(UpstreamNetworkState ns) {
@@ -1715,12 +1711,16 @@
         private final ArrayList<IpServer> mNotifyList;
         private final IPv6TetheringCoordinator mIPv6TetheringCoordinator;
         private final OffloadWrapper mOffload;
+        // TODO: Figure out how to merge this and other downstream-tracking objects
+        // into a single coherent structure.
+        private final HashSet<IpServer> mForwardedDownstreams;
 
         private static final int UPSTREAM_SETTLE_TIME_MS     = 10000;
 
         TetherMainSM(String name, Looper looper, TetheringDependencies deps) {
             super(name, looper);
 
+            mForwardedDownstreams = new HashSet<>();
             mInitialState = new InitialState();
             mTetherModeAliveState = new TetherModeAliveState();
             mSetIpForwardingEnabledErrorState = new SetIpForwardingEnabledErrorState();
@@ -2056,6 +2056,10 @@
             }
         }
 
+        private boolean upstreamWanted() {
+            return !mForwardedDownstreams.isEmpty();
+        }
+
         class TetherModeAliveState extends State {
             boolean mUpstreamWanted = false;
             boolean mTryCell = true;
@@ -2078,9 +2082,6 @@
                     chooseUpstreamType(true);
                     mTryCell = false;
                 }
-
-                // TODO: Check the upstream interface if it is managed by BPF offload.
-                mBpfCoordinator.startPolling();
             }
 
             @Override
@@ -2094,7 +2095,6 @@
                     reportUpstreamChanged(null);
                     mNotificationUpdater.onUpstreamCapabilitiesChanged(null);
                 }
-                mBpfCoordinator.stopPolling();
                 mTetheringMetrics.cleanup();
             }
 
@@ -2430,9 +2430,6 @@
 
     /** Unregister tethering event callback */
     void unregisterTetheringEventCallback(ITetheringEventCallback callback) {
-        if (callback == null) {
-            throw new NullPointerException();
-        }
         mHandler.post(() -> {
             mTetheringEventCallbacks.unregister(callback);
         });
@@ -2651,7 +2648,7 @@
             }
             pw.println(" - lastError = " + tetherState.lastError);
         }
-        pw.println("Upstream wanted: " + upstreamWanted());
+        pw.println("Upstream wanted: " + mTetherMainSM.upstreamWanted());
         pw.println("Current upstream interface(s): " + mCurrentUpstreamIfaceSet);
         pw.decreaseIndent();
 
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 9dfd225..5d9d349 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -22,7 +22,6 @@
 import android.bluetooth.BluetoothPan;
 import android.content.Context;
 import android.net.INetd;
-import android.net.RoutingCoordinatorManager;
 import android.net.connectivity.ConnectivityInternalApiUtil;
 import android.net.ip.IpServer;
 import android.os.Build;
@@ -36,7 +35,8 @@
 import androidx.annotation.RequiresApi;
 
 import com.android.modules.utils.build.SdkLevel;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorManager;
+import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.BluetoothPanShimImpl;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -120,19 +120,26 @@
     /**
      * Get a reference to INetd to be used by tethering.
      */
-    public INetd getINetd(Context context) {
-        return INetd.Stub.asInterface(
-                (IBinder) context.getSystemService(Context.NETD_SERVICE));
+    public INetd getINetd(Context context, SharedLog log) {
+        final INetd netd =
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE));
+        if (netd == null) {
+            log.wtf("INetd is null");
+        }
+        return netd;
     }
 
     /**
-     * Get the routing coordinator, or null if below S.
+     * Get the routing coordinator.
      */
-    @Nullable
-    public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(Context context) {
-        if (!SdkLevel.isAtLeastS()) return new LateSdk<>(null);
-        return new LateSdk<>(
-                ConnectivityInternalApiUtil.getRoutingCoordinatorManager(context));
+    public RoutingCoordinatorManager getRoutingCoordinator(Context context, SharedLog log) {
+        IBinder binder;
+        if (!SdkLevel.isAtLeastS()) {
+            binder = new RoutingCoordinatorService(getINetd(context, log));
+        } else {
+            binder = ConnectivityInternalApiUtil.getRoutingCoordinator(context);
+        }
+        return new RoutingCoordinatorManager(context, binder);
     }
 
     /**
@@ -156,6 +163,7 @@
     /**
      * Get a reference to BluetoothAdapter to be used by tethering.
      */
+    @Nullable
     public abstract BluetoothAdapter getBluetoothAdapter();
 
     /**
@@ -185,8 +193,8 @@
     /**
      * Make the TetheringMetrics to be used by tethering.
      */
-    public TetheringMetrics makeTetheringMetrics() {
-        return new TetheringMetrics();
+    public TetheringMetrics makeTetheringMetrics(Context ctx) {
+        return new TetheringMetrics(ctx);
     }
 
     /**
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index aa73819..454cbf1 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -30,6 +30,7 @@
 
 import android.app.Service;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
 import android.content.Context;
 import android.content.Intent;
 import android.net.IIntResultListener;
@@ -37,6 +38,7 @@
 import android.net.ITetheringConnector;
 import android.net.ITetheringEventCallback;
 import android.net.NetworkStack;
+import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringRequestParcel;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
@@ -136,8 +138,8 @@
                     listener)) {
                 return;
             }
-
-            mTethering.startTethering(request, callerPkg, listener);
+            // TODO(b/216524590): Add UID/packageName of caller to TetheringRequest here
+            mTethering.startTethering(new TetheringRequest(request), callerPkg, listener);
         }
 
         @Override
@@ -162,6 +164,8 @@
         @Override
         public void registerTetheringEventCallback(ITetheringEventCallback callback,
                 String callerPkg) {
+            // Silently ignore call if the callback is null. This can only happen via reflection.
+            if (callback == null) return;
             try {
                 if (!hasTetherAccessPermission()) {
                     callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
@@ -174,6 +178,8 @@
         @Override
         public void unregisterTetheringEventCallback(ITetheringEventCallback callback,
                 String callerPkg) {
+            // Silently ignore call if the callback is null. This can only happen via reflection.
+            if (callback == null) return;
             try {
                 if (!hasTetherAccessPermission()) {
                     callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
@@ -377,7 +383,11 @@
 
             @Override
             public BluetoothAdapter getBluetoothAdapter() {
-                return BluetoothAdapter.getDefaultAdapter();
+                final BluetoothManager btManager = getSystemService(BluetoothManager.class);
+                if (btManager == null) {
+                    return null;
+                }
+                return btManager.getAdapter();
             }
         };
     }
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 814afcd..136dfb1 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -46,6 +46,7 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
 import android.annotation.Nullable;
+import android.content.Context;
 import android.net.NetworkCapabilities;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
@@ -81,16 +82,50 @@
     private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
     private final SparseArray<Long> mDownstreamStartTime = new SparseArray<Long>();
     private final ArrayList<RecordUpstreamEvent> mUpstreamEventList = new ArrayList<>();
+    private final Context mContext;
+    private final Dependencies mDependencies;
     private UpstreamType mCurrentUpstream = null;
     private Long mCurrentUpStreamStartTime = 0L;
 
+    /**
+     * Dependencies of TetheringMetrics, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * @see TetheringStatsLog
+         */
+        public void write(NetworkTetheringReported reported) {
+            TetheringStatsLog.write(
+                    TetheringStatsLog.NETWORK_TETHERING_REPORTED,
+                    reported.getErrorCode().getNumber(),
+                    reported.getDownstreamType().getNumber(),
+                    reported.getUpstreamType().getNumber(),
+                    reported.getUserType().getNumber(),
+                    reported.getUpstreamEvents().toByteArray(),
+                    reported.getDurationMillis());
+        }
+
+        /**
+         * @see System#currentTimeMillis()
+         */
+        public long timeNow() {
+            return System.currentTimeMillis();
+        }
+    }
 
     /**
-     * Return the current system time in milliseconds.
-     * @return the current system time in milliseconds.
+     * Constructor for the TetheringMetrics class.
+     *
+     * @param context The Context object used to access system services.
      */
-    public long timeNow() {
-        return System.currentTimeMillis();
+    public TetheringMetrics(Context context) {
+        this(context, new Dependencies());
+    }
+
+    TetheringMetrics(Context context, Dependencies dependencies) {
+        mContext = context;
+        mDependencies = dependencies;
     }
 
     private static class RecordUpstreamEvent {
@@ -123,7 +158,7 @@
                 .setUpstreamEvents(UpstreamEvents.newBuilder())
                 .setDurationMillis(0);
         mBuilderMap.put(downstreamType, statsBuilder);
-        mDownstreamStartTime.put(downstreamType, timeNow());
+        mDownstreamStartTime.put(downstreamType, mDependencies.timeNow());
     }
 
     /**
@@ -149,7 +184,7 @@
         UpstreamType upstream = transportTypeToUpstreamTypeEnum(ns);
         if (upstream.equals(mCurrentUpstream)) return;
 
-        final long newTime = timeNow();
+        final long newTime = mDependencies.timeNow();
         if (mCurrentUpstream != null) {
             mUpstreamEventList.add(new RecordUpstreamEvent(mCurrentUpStreamStartTime, newTime,
                     mCurrentUpstream));
@@ -206,7 +241,7 @@
                     event.mUpstreamType, 0L /* txBytes */, 0L /* rxBytes */);
         }
         final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
-        final long stopTime = timeNow();
+        final long stopTime = mDependencies.timeNow();
         // Handle the last upstream event.
         addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream,
                 0L /* txBytes */, 0L /* rxBytes */);
@@ -248,15 +283,7 @@
     @VisibleForTesting
     public void write(@NonNull final NetworkTetheringReported reported) {
         final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
-
-        TetheringStatsLog.write(
-                TetheringStatsLog.NETWORK_TETHERING_REPORTED,
-                reported.getErrorCode().getNumber(),
-                reported.getDownstreamType().getNumber(),
-                reported.getUpstreamType().getNumber(),
-                reported.getUserType().getNumber(),
-                upstreamEvents,
-                reported.getDurationMillis());
+        mDependencies.write(reported);
         if (DBG) {
             Log.d(
                     TAG,
@@ -374,4 +401,22 @@
 
         return UpstreamType.UT_UNKNOWN;
     }
+
+    /**
+     * Check whether tethering metrics' data usage can be collected for a given upstream type.
+     *
+     * @param type the upstream type
+     */
+    public static boolean isUsageSupportedForUpstreamType(@NonNull UpstreamType type) {
+        switch(type) {
+            case UT_CELLULAR:
+            case UT_WIFI:
+            case UT_BLUETOOTH:
+            case UT_ETHERNET:
+                return true;
+            default:
+                break;
+        }
+        return false;
+    }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
index 078a35f..c236188 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
@@ -22,7 +22,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
+import com.android.net.module.util.SyncStateMachine;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
 
 import java.util.List;
 
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index e6236df..76c2f0d 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -16,7 +16,6 @@
 package com.android.networkstack.tethering.util;
 
 import android.net.TetherStatsParcel;
-import android.net.TetheringRequestParcel;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -29,7 +28,6 @@
 import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.util.Arrays;
-import java.util.Objects;
 
 /**
  * The classes and the methods for tethering utilization.
@@ -158,20 +156,6 @@
         return s & 0xffff;
     }
 
-    /** Check whether two TetheringRequestParcels are the same. */
-    public static boolean isTetheringRequestEquals(final TetheringRequestParcel request,
-            final TetheringRequestParcel otherRequest) {
-        if (request == otherRequest) return true;
-
-        return request != null && otherRequest != null
-                && request.tetheringType == otherRequest.tetheringType
-                && Objects.equals(request.localIPv4Address, otherRequest.localIPv4Address)
-                && Objects.equals(request.staticClientAddress, otherRequest.staticClientAddress)
-                && request.exemptFromEntitlementCheck == otherRequest.exemptFromEntitlementCheck
-                && request.showProvisioningUi == otherRequest.showProvisioningUi
-                && request.connectivityScope == otherRequest.connectivityScope;
-    }
-
     /** Get inet6 address for all nodes given scope ID. */
     public static Inet6Address getAllNodesForScopeId(int scopeId) {
         try {
diff --git a/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java b/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
index c9e75c0..5eb1551 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/VersionedBroadcastListener.java
@@ -62,8 +62,8 @@
         if (DBG) Log.d(mTag, "startListening");
         if (mReceiver != null) return;
 
-        mReceiver = new Receiver(mTag, mGenerationNumber, mCallback);
-        mContext.registerReceiver(mReceiver, mFilter, null, mHandler);
+        mReceiver = new Receiver(mTag, mGenerationNumber, mCallback, mHandler);
+        mContext.registerReceiver(mReceiver, mFilter);
     }
 
     /** Stop listening to intent broadcast. */
@@ -77,30 +77,35 @@
     }
 
     private static class Receiver extends BroadcastReceiver {
-        public final String tag;
-        public final AtomicInteger atomicGenerationNumber;
-        public final Consumer<Intent> callback;
+        final String mTag;
+        final AtomicInteger mAtomicGenerationNumber;
+        final Consumer<Intent> mCallback;
         // Used to verify this receiver is still current.
-        public final int generationNumber;
+        final int mGenerationNumber;
+        private final Handler mHandler;
 
-        Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback) {
-            this.tag = tag;
-            this.atomicGenerationNumber = atomicGenerationNumber;
-            this.callback = callback;
-            generationNumber = atomicGenerationNumber.incrementAndGet();
+        Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback,
+                Handler handler) {
+            mTag = tag;
+            mAtomicGenerationNumber = atomicGenerationNumber;
+            mCallback = callback;
+            mGenerationNumber = atomicGenerationNumber.incrementAndGet();
+            mHandler = handler;
         }
 
         @Override
         public void onReceive(Context context, Intent intent) {
-            final int currentGenerationNumber = atomicGenerationNumber.get();
+            mHandler.post(() -> {
+                final int currentGenerationNumber = mAtomicGenerationNumber.get();
 
-            if (DBG) {
-                Log.d(tag, "receiver generationNumber=" + generationNumber
-                        + ", current generationNumber=" + currentGenerationNumber);
-            }
-            if (generationNumber != currentGenerationNumber) return;
+                if (DBG) {
+                    Log.d(mTag, "receiver generationNumber=" + mGenerationNumber
+                            + ", current generationNumber=" + currentGenerationNumber);
+                }
+                if (mGenerationNumber != currentGenerationNumber) return;
 
-            callback.accept(intent);
+                mCallback.accept(intent);
+            });
         }
     }
 }
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 07fa733..337d408 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -33,6 +33,7 @@
         "net-tests-utils",
         "net-utils-device-common",
         "net-utils-device-common-bpf",
+        "net-utils-device-common-struct-base",
         "testables",
         "connectivity-net-module-utils-bpf",
     ],
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 2933a44..3944a8a 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -16,7 +16,6 @@
 
 package android.net;
 
-import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.TETHER_PRIVILEGED;
@@ -42,7 +41,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -65,7 +63,6 @@
 import androidx.annotation.NonNull;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.testutils.HandlerUtils;
@@ -74,7 +71,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.BeforeClass;
 
 import java.io.FileDescriptor;
 import java.net.Inet4Address;
@@ -83,8 +79,10 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -104,7 +102,7 @@
     // Used to check if any tethering interface is available. Choose 200ms to be request timeout
     // because the average interface requested time on cuttlefish@acloud is around 10ms.
     // See TetheredInterfaceRequester.getInterface, isInterfaceForTetheringAvailable.
-    private static final int AVAILABLE_TETHER_IFACE_REQUEST_TIMEOUT_MS = 200;
+    private static final int SHORT_TIMEOUT_MS = 1000;
     private static final int TETHER_REACHABILITY_ATTEMPTS = 20;
     protected static final long WAIT_RA_TIMEOUT_MS = 2000;
 
@@ -142,16 +140,18 @@
 
     private static final Context sContext =
             InstrumentationRegistry.getInstrumentation().getContext();
-    private static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class);
+    protected static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class);
     private static final TetheringManager sTm = sContext.getSystemService(TetheringManager.class);
     private static final PackageManager sPackageManager = sContext.getPackageManager();
     private static final CtsNetUtils sCtsNetUtils = new CtsNetUtils(sContext);
+    private static final List<String> sCallbackErrors =
+            Collections.synchronizedList(new ArrayList<>());
 
     // Late initialization in setUp()
     private boolean mRunTests;
     private HandlerThread mHandlerThread;
     private Handler mHandler;
-    private TetheredInterfaceRequester mTetheredInterfaceRequester;
+    protected TetheredInterfaceRequester mTetheredInterfaceRequester;
 
     // Late initialization in initTetheringTester().
     private TapPacketReader mUpstreamReader;
@@ -164,33 +164,6 @@
         return sContext;
     }
 
-    @BeforeClass
-    public static void setUpOnce() throws Exception {
-        // The first test case may experience tethering restart with IP conflict handling.
-        // Tethering would cache the last upstreams so that the next enabled tethering avoids
-        // picking up the address that is in conflict with the upstreams. To protect subsequent
-        // tests, turn tethering on and off before running them.
-        MyTetheringEventCallback callback = null;
-        TestNetworkInterface testIface = null;
-        try {
-            // If the physical ethernet interface is available, do nothing.
-            if (isInterfaceForTetheringAvailable()) return;
-
-            testIface = createTestInterface();
-            setIncludeTestInterfaces(true);
-
-            callback = enableEthernetTethering(testIface.getInterfaceName(), null);
-            callback.awaitUpstreamChanged(true /* throwTimeoutException */);
-        } catch (TimeoutException e) {
-            Log.d(TAG, "WARNNING " + e);
-        } finally {
-            maybeCloseTestInterface(testIface);
-            maybeUnregisterTetheringEventCallback(callback);
-
-            setIncludeTestInterfaces(false);
-        }
-    }
-
     @Before
     public void setUp() throws Exception {
         mHandlerThread = new HandlerThread(getClass().getSimpleName());
@@ -201,6 +174,7 @@
         assumeTrue(mRunTests);
 
         mTetheredInterfaceRequester = new TetheredInterfaceRequester();
+        sCallbackErrors.clear();
     }
 
     private boolean isEthernetTetheringSupported() throws Exception {
@@ -268,47 +242,37 @@
         maybeUnregisterTetheringEventCallback(mTetheringEventCallback);
         mTetheringEventCallback = null;
 
-        runAsShell(NETWORK_SETTINGS, () -> mTetheredInterfaceRequester.release());
         setIncludeTestInterfaces(false);
     }
 
     @After
     public void tearDown() throws Exception {
+        if (mTetheredInterfaceRequester != null) {
+            mTetheredInterfaceRequester.release();
+        }
         try {
             if (mRunTests) cleanUp();
         } finally {
             mHandlerThread.quitSafely();
             mHandlerThread.join();
         }
+
+        if (sCallbackErrors.size() > 0) {
+            fail("Some callbacks had errors: " + sCallbackErrors);
+        }
     }
 
-    protected static boolean isInterfaceForTetheringAvailable() throws Exception {
-        // Before T, all ethernet interfaces could be used for server mode. Instead of
-        // waiting timeout, just checking whether the system currently has any
-        // ethernet interface is more reliable.
-        if (!SdkLevel.isAtLeastT()) {
-            return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> sEm.isAvailable());
-        }
-
+    protected boolean isInterfaceForTetheringAvailable() throws Exception {
         // If previous test case doesn't release tethering interface successfully, the other tests
         // after that test may be skipped as unexcepted.
         // TODO: figure out a better way to check default tethering interface existenion.
-        final TetheredInterfaceRequester requester = new TetheredInterfaceRequester();
-        try {
-            // Use short timeout (200ms) for requesting an existing interface, if any, because
-            // it should reurn faster than requesting a new tethering interface. Using default
-            // timeout (5000ms, TIMEOUT_MS) may make that total testing time is over 1 minute
-            // test module timeout on internal testing.
-            // TODO: if this becomes flaky, consider using default timeout (5000ms) and moving
-            // this check into #setUpOnce.
-            return requester.getInterface(AVAILABLE_TETHER_IFACE_REQUEST_TIMEOUT_MS) != null;
-        } catch (TimeoutException e) {
-            return false;
-        } finally {
-            runAsShell(NETWORK_SETTINGS, () -> {
-                requester.release();
-            });
-        }
+        // Use short timeout (200ms) for requesting an existing interface, if any, because
+        // it should reurn faster than requesting a new tethering interface. Using default
+        // timeout (5000ms, TIMEOUT_MS) may make that total testing time is over 1 minute
+        // test module timeout on internal testing.
+        // TODO: if this becomes flaky, consider using default timeout (5000ms) and moving
+        // this check into #setUpOnce.
+        return mTetheredInterfaceRequester.isPhysicalInterfaceAvailable(SHORT_TIMEOUT_MS);
     }
 
     protected static void setIncludeTestInterfaces(boolean include) {
@@ -323,14 +287,6 @@
         });
     }
 
-    protected String getTetheredInterface() throws Exception {
-        return mTetheredInterfaceRequester.getInterface();
-    }
-
-    protected CompletableFuture<String> requestTetheredInterface() throws Exception {
-        return mTetheredInterfaceRequester.requestInterface();
-    }
-
     protected static void waitForRouterAdvertisement(TapPacketReader reader, String iface,
             long timeoutMs) {
         final long deadline = SystemClock.uptimeMillis() + timeoutMs;
@@ -391,7 +347,7 @@
         }
         @Override
         public void onTetheredInterfacesChanged(List<String> interfaces) {
-            fail("Should only call callback that takes a Set<TetheringInterface>");
+            addCallbackError("Should only call callback that takes a Set<TetheringInterface>");
         }
 
         @Override
@@ -412,7 +368,7 @@
 
         @Override
         public void onLocalOnlyInterfacesChanged(List<String> interfaces) {
-            fail("Should only call callback that takes a Set<TetheringInterface>");
+            addCallbackError("Should only call callback that takes a Set<TetheringInterface>");
         }
 
         @Override
@@ -481,7 +437,7 @@
             // Ignore stale callbacks registered by previous test cases.
             if (mUnregistered) return;
 
-            fail("TetheringEventCallback got error:" + error + " on iface " + ifName);
+            addCallbackError("TetheringEventCallback got error:" + error + " on iface " + ifName);
         }
 
         @Override
@@ -536,6 +492,11 @@
         }
     }
 
+    private static void addCallbackError(String error) {
+        Log.e(TAG, error);
+        sCallbackErrors.add(error);
+    }
+
     protected static MyTetheringEventCallback enableEthernetTethering(String iface,
             TetheringRequest request, Network expectedUpstream) throws Exception {
         // Enable ethernet tethering with null expectedUpstream means the test accept any upstream
@@ -562,7 +523,7 @@
 
             @Override
             public void onTetheringFailed(int resultCode) {
-                fail("Unexpectedly got onTetheringFailed");
+                addCallbackError("Unexpectedly got onTetheringFailed");
             }
         };
         Log.d(TAG, "Starting Ethernet tethering");
@@ -619,6 +580,11 @@
         private TetheredInterfaceRequest mRequest;
         private final CompletableFuture<String> mFuture = new CompletableFuture<>();
 
+        TetheredInterfaceRequester() {
+            mRequest = runAsShell(NETWORK_SETTINGS, () ->
+                    sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
+        }
+
         @Override
         public void onAvailable(String iface) {
             Log.d(TAG, "Ethernet interface available: " + iface);
@@ -630,28 +596,21 @@
             mFuture.completeExceptionally(new IllegalStateException("onUnavailable received"));
         }
 
-        public CompletableFuture<String> requestInterface() {
-            assertNull("BUG: more than one tethered interface request", mRequest);
-            Log.d(TAG, "Requesting tethered interface");
-            mRequest = runAsShell(NETWORK_SETTINGS, () ->
-                    sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
-            return mFuture;
-        }
-
-        public String getInterface(int timeout) throws Exception {
-            return requestInterface().get(timeout, TimeUnit.MILLISECONDS);
+        public boolean isPhysicalInterfaceAvailable(int timeout) {
+            try {
+                final String iface = mFuture.get(timeout, TimeUnit.MILLISECONDS);
+                return !iface.startsWith("testtap");
+            } catch (Exception e) {
+                return false;
+            }
         }
 
         public String getInterface() throws Exception {
-            return getInterface(TIMEOUT_MS);
+            return mFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
         }
 
         public void release() {
-            if (mRequest != null) {
-                mFuture.obtrudeException(new IllegalStateException("Request already released"));
-                mRequest.release();
-                mRequest = null;
-            }
+            runAsShell(NETWORK_SETTINGS, () -> mRequest.release());
         }
     }
 
@@ -672,7 +631,10 @@
         lp.setLinkAddresses(addresses);
         lp.setDnsServers(dnses);
 
-        return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
+        // TODO: initTestNetwork can take up to 15 seconds on a workstation. Investigate when and
+        // why this is the case. It is unclear whether a 30 second timeout is enough when running
+        // these tests in the much slower test infra.
+        return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, 30_000));
     }
 
     protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 4949eaa..9cdba2f 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -16,6 +16,7 @@
 
 package android.net;
 
+import static android.Manifest.permission.DUMP;
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
@@ -26,39 +27,55 @@
 import static android.net.TetheringTester.isExpectedUdpDnsPacket;
 import static android.system.OsConstants.ICMP_ECHO;
 import static android.system.OsConstants.ICMP_ECHOREPLY;
+import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
 import static com.android.net.module.util.HexDump.dumpHexString;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.testutils.DeviceInfoUtils.KVersion;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
+import android.content.Context;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringTester.TetheredDevice;
 import android.os.Build;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
 import android.util.Log;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.bpf.Tether4Key;
+import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
 import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.UdpHeader;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceInfoUtils;
+import com.android.testutils.DumpTestUtils;
 import com.android.testutils.NetworkStackModuleTest;
 import com.android.testutils.TapPacketReader;
 
+import org.junit.After;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -72,10 +89,10 @@
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
 @RunWith(AndroidJUnit4.class)
@@ -90,6 +107,26 @@
     private static final short ICMPECHO_ID = 0x0;
     private static final short ICMPECHO_SEQ = 0x0;
 
+    private static final int DUMP_POLLING_MAX_RETRY = 100;
+    private static final int DUMP_POLLING_INTERVAL_MS = 50;
+    // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
+    // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
+    private static final int UDP_STREAM_TS_MS = 2000;
+    // Give slack time for waiting UDP stream mode because handling conntrack event in user space
+    // may not in precise time. Used to reduce the flaky rate.
+    private static final int UDP_STREAM_SLACK_MS = 500;
+    // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+    private static final int RX_UDP_PACKET_SIZE = 30;
+    private static final int RX_UDP_PACKET_COUNT = 456;
+    // Per TX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+    private static final int TX_UDP_PACKET_SIZE = 30;
+    private static final int TX_UDP_PACKET_COUNT = 123;
+
+    private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
+    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+    private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
+    private static final String LINE_DELIMITER = "\\n";
+
     // TODO: use class DnsPacket to build DNS query and reply message once DnsPacket supports
     // building packet for given arguments.
     private static final ByteBuffer DNS_QUERY = ByteBuffer.wrap(new byte[] {
@@ -150,6 +187,16 @@
             (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04  /* Address: 1.2.3.4 */
     };
 
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+        // TODO: See b/318121782#comment4. Register an ethernet InterfaceStateListener, and wait for
+        // the callback to report client mode. This happens as soon as both
+        // TetheredInterfaceRequester and the tethering code itself have released the interface,
+        // i.e. after stopTethering() has completed.
+        Thread.sleep(3000);
+    }
+
     @Test
     public void testVirtualEthernetAlreadyExists() throws Exception {
         // This test requires manipulating packets. Skip if there is a physical Ethernet connected.
@@ -171,7 +218,7 @@
             Log.d(TAG, "Including test interfaces");
             setIncludeTestInterfaces(true);
 
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -193,8 +240,6 @@
         // This test requires manipulating packets. Skip if there is a physical Ethernet connected.
         assumeFalse(isInterfaceForTetheringAvailable());
 
-        CompletableFuture<String> futureIface = requestTetheredInterface();
-
         setIncludeTestInterfaces(true);
 
         TestNetworkInterface downstreamIface = null;
@@ -204,7 +249,7 @@
         try {
             downstreamIface = createTestInterface();
 
-            final String iface = futureIface.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -234,7 +279,7 @@
         try {
             downstreamIface = createTestInterface();
 
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -308,7 +353,7 @@
         try {
             downstreamIface = createTestInterface();
 
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
             assertEquals("TetheredInterfaceCallback for unexpected interface",
                     downstreamIface.getInterfaceName(), iface);
 
@@ -358,7 +403,7 @@
         MyTetheringEventCallback tetheringEventCallback = null;
         try {
             // Get an interface to use.
-            final String iface = getTetheredInterface();
+            final String iface = mTetheredInterfaceRequester.getInterface();
 
             // Enable Ethernet tethering and check that it starts.
             tetheringEventCallback = enableEthernetTethering(iface, null /* any upstream */);
@@ -479,17 +524,23 @@
         // TODO: test BPF offload maps {rule, stats}.
     }
 
-    // Test network topology:
-    //
-    //         public network (rawip)                 private network
-    //                   |                 UE                |
-    // +------------+    V    +------------+------------+    V    +------------+
-    // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
-    // +------------+         +------------+------------+         +------------+
-    // remote ip              public ip                           private ip
-    // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
-    //
-    private void runUdp4Test() throws Exception {
+
+    /**
+     * Basic IPv4 UDP tethering test. Verify that UDP tethered packets are transferred no matter
+     * using which data path.
+     */
+    @Test
+    public void testTetherUdpV4() throws Exception {
+        // Test network topology:
+        //
+        //         public network (rawip)                 private network
+        //                   |                 UE                |
+        // +------------+    V    +------------+------------+    V    +------------+
+        // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
+        // +------------+         +------------+------------+         +------------+
+        // remote ip              public ip                           private ip
+        // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
+        //
         final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
                 toList(TEST_IP4_DNS));
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
@@ -511,15 +562,6 @@
         sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
     }
 
-    /**
-     * Basic IPv4 UDP tethering test. Verify that UDP tethered packets are transferred no matter
-     * using which data path.
-     */
-    @Test
-    public void testTetherUdpV4() throws Exception {
-        runUdp4Test();
-    }
-
     // Test network topology:
     //
     //            public network (rawip)                 private network
@@ -569,7 +611,7 @@
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
 
         // TODO: remove the connectivity verification for upstream connected notification race.
-        // See the same reason in runUdp4Test().
+        // See the same reason in testTetherUdp4().
         probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         final ByteBuffer request = buildIcmpEchoPacketV4(tethered.macAddr /* srcMac */,
@@ -677,7 +719,7 @@
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
 
         // TODO: remove the connectivity verification for upstream connected notification race.
-        // See the same reason in runUdp4Test().
+        // See the same reason in testTetherUdp4().
         probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         // [1] Send DNS query.
@@ -721,7 +763,7 @@
         final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
 
         // TODO: remove the connectivity verification for upstream connected notification race.
-        // See the same reason in runUdp4Test().
+        // See the same reason in testTetherUdp4().
         probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         runTcpTest(tethered.macAddr /* uploadSrcMac */, tethered.routerMacAddr /* uploadDstMac */,
@@ -798,4 +840,217 @@
         final MacAddress macAddress = MacAddress.fromString("11:22:33:44:55:66");
         assertTrue(tester.testDhcpServerAlive(macAddress));
     }
+
+    private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
+        final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
+        return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
+                || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
+                || current.isAtLeast(new KVersion(5, 4, 98));
+    }
+
+    @Test
+    public void testIsUdpOffloadSupportedByKernel() throws Exception {
+        assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
+        assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
+
+        assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
+        assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
+        assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
+
+        assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
+        assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
+    }
+
+    private static void assumeKernelSupportBpfOffloadUdpV4() {
+        final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
+        assumeTrue("Kernel version " + kernelVersion + " doesn't support IPv4 UDP BPF offload",
+                isUdpOffloadSupportedByKernel(kernelVersion));
+    }
+
+    @Test
+    public void testKernelSupportBpfOffloadUdpV4() throws Exception {
+        assumeKernelSupportBpfOffloadUdpV4();
+    }
+
+    private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
+        final String dumpStr = runAsShell(DUMP, () ->
+                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short"));
+
+        // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
+        // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
+        // RRO to override the enabled default value. Get the tethering config via dumpsys.
+        // $ dumpsys tethering
+        //   mIsBpfEnabled: true
+        boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
+        if (!enabled) {
+            Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
+        }
+        return enabled;
+    }
+
+    @Test
+    public void testTetherConfigBpfOffloadEnabled() throws Exception {
+        assumeTrue(isTetherConfigBpfOffloadEnabled());
+    }
+
+    @NonNull
+    private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
+        final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
+        final String rawMapStr = runAsShell(DUMP, () ->
+                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args));
+        final HashMap<K, V> map = new HashMap<>();
+
+        for (final String line : rawMapStr.split(LINE_DELIMITER)) {
+            final Pair<K, V> rule =
+                    BpfDump.fromBase64EncodedString(keyClass, valueClass, line.trim());
+            map.put(rule.first, rule.second);
+        }
+        return map;
+    }
+
+    @Nullable
+    private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            throws Exception {
+        for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
+            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
+            if (!map.isEmpty()) return map;
+
+            Thread.sleep(DUMP_POLLING_INTERVAL_MS);
+        }
+
+        fail("Cannot get rules after " + DUMP_POLLING_MAX_RETRY * DUMP_POLLING_INTERVAL_MS + "ms");
+        return null;
+    }
+
+    // Test network topology:
+    //
+    //         public network (rawip)                 private network
+    //                   |                 UE                |
+    // +------------+    V    +------------+------------+    V    +------------+
+    // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
+    // +------------+         +------------+------------+         +------------+
+    // remote ip              public ip                           private ip
+    // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
+    //
+    private void runUdp4Test() throws Exception {
+        final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
+                toList(TEST_IP4_DNS));
+        final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
+
+        // TODO: remove the connectivity verification for upstream connected notification race.
+        // Because async upstream connected notification can't guarantee the tethering routing is
+        // ready to use. Need to test tethering connectivity before testing.
+        // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
+        // from upstream. That can guarantee that the routing is ready. Long term plan is that
+        // refactors upstream connected notification from async to sync.
+        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
+
+        final MacAddress srcMac = tethered.macAddr;
+        final MacAddress dstMac = tethered.routerMacAddr;
+        final InetAddress remoteIp = REMOTE_IP4_ADDR;
+        final InetAddress tetheringUpstreamIp = TEST_IP4_ADDR.getAddress();
+        final InetAddress clientIp = tethered.ipv4Addr;
+        sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
+        sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
+
+        // Send second UDP packet in original direction.
+        // The BPF coordinator only offloads the ASSURED conntrack entry. The "request + reply"
+        // packets can make status IPS_SEEN_REPLY to be set. Need one more packet to make
+        // conntrack status IPS_ASSURED_BIT to be set. Note the third packet needs to delay
+        // 2 seconds because kernel monitors a UDP connection which still alive after 2 seconds
+        // and apply ASSURED flag.
+        // See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
+        // nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
+        Thread.sleep(UDP_STREAM_TS_MS);
+        sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
+
+        // Give a slack time for handling conntrack event in user space.
+        Thread.sleep(UDP_STREAM_SLACK_MS);
+
+        // [1] Verify IPv4 upstream rule map.
+        final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
+                Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
+        assertNotNull(upstreamMap);
+        assertEquals(1, upstreamMap.size());
+
+        final Map.Entry<Tether4Key, Tether4Value> rule =
+                upstreamMap.entrySet().iterator().next();
+
+        final Tether4Key upstream4Key = rule.getKey();
+        assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
+        assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
+        assertEquals(LOCAL_PORT, upstream4Key.srcPort);
+        assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
+        assertEquals(REMOTE_PORT, upstream4Key.dstPort);
+
+        final Tether4Value upstream4Value = rule.getValue();
+        assertTrue(Arrays.equals(tetheringUpstreamIp.getAddress(),
+                InetAddress.getByAddress(upstream4Value.src46).getAddress()));
+        assertEquals(LOCAL_PORT, upstream4Value.srcPort);
+        assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
+                InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
+        assertEquals(REMOTE_PORT, upstream4Value.dstPort);
+
+        // [2] Verify stats map.
+        // Transmit packets on both direction for verifying stats. Because we only care the
+        // packet count in stats test, we just reuse the existing packets to increaes
+        // the packet count on both direction.
+
+        // Send packets on original direction.
+        for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+            sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester,
+                    false /* is4To6 */);
+        }
+
+        // Send packets on reply direction.
+        for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+            sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
+        }
+
+        // Dump stats map to verify.
+        final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
+                TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+        assertNotNull(statsMap);
+        assertEquals(1, statsMap.size());
+
+        final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
+                statsMap.entrySet().iterator().next();
+
+        // TODO: verify the upstream index in TetherStatsKey.
+
+        final TetherStatsValue statsValue = stats.getValue();
+        assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
+        assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
+        assertEquals(0, statsValue.rxErrors);
+        assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
+        assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
+        assertEquals(0, statsValue.txErrors);
+    }
+
+    /**
+     * BPF offload IPv4 UDP tethering test. Verify that UDP tethered packets are offloaded by BPF.
+     * Minimum test requirement:
+     * 1. S+ device.
+     * 2. Tethering config enables tethering BPF offload.
+     * 3. Kernel supports IPv4 UDP BPF offload. See #isUdpOffloadSupportedByKernel.
+     *
+     * TODO: consider enabling the test even tethering config disables BPF offload. See b/238288883
+     */
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testTetherBpfOffloadUdpV4() throws Exception {
+        assumeTrue("Tethering config disabled BPF offload", isTetherConfigBpfOffloadEnabled());
+        assumeKernelSupportBpfOffloadUdpV4();
+
+        runUdp4Test();
+    }
 }
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index a80e49e..c4d5636 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -45,6 +45,7 @@
         "junit-params",
         "connectivity-net-module-utils-bpf",
         "net-utils-device-common-bpf",
+        "net-utils-device-common-struct-base",
     ],
 
     jni_libs: [
diff --git a/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java b/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java
deleted file mode 100644
index c2bc812..0000000
--- a/Tethering/tests/mts/src/android/tethering/mts/MtsEthernetTetheringTest.java
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * 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 android.net;
-
-import static android.Manifest.permission.DUMP;
-import static android.system.OsConstants.IPPROTO_UDP;
-
-import static com.android.testutils.DeviceInfoUtils.KVersion;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Context;
-import android.net.TetheringTester.TetheredDevice;
-import android.os.Build;
-import android.os.VintfRuntimeInfo;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.net.module.util.BpfDump;
-import com.android.net.module.util.Struct;
-import com.android.net.module.util.bpf.Tether4Key;
-import com.android.net.module.util.bpf.Tether4Value;
-import com.android.net.module.util.bpf.TetherStatsKey;
-import com.android.net.module.util.bpf.TetherStatsValue;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.DeviceInfoUtils;
-import com.android.testutils.DumpTestUtils;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.net.InetAddress;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-
-@RunWith(AndroidJUnit4.class)
-@MediumTest
-public class MtsEthernetTetheringTest extends EthernetTetheringTestBase {
-    @Rule
-    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
-    private static final String TAG = MtsEthernetTetheringTest.class.getSimpleName();
-
-    private static final int DUMP_POLLING_MAX_RETRY = 100;
-    private static final int DUMP_POLLING_INTERVAL_MS = 50;
-    // Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
-    // See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
-    private static final int UDP_STREAM_TS_MS = 2000;
-    // Give slack time for waiting UDP stream mode because handling conntrack event in user space
-    // may not in precise time. Used to reduce the flaky rate.
-    private static final int UDP_STREAM_SLACK_MS = 500;
-    // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
-    private static final int RX_UDP_PACKET_SIZE = 30;
-    private static final int RX_UDP_PACKET_COUNT = 456;
-    // Per TX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
-    private static final int TX_UDP_PACKET_SIZE = 30;
-    private static final int TX_UDP_PACKET_COUNT = 123;
-
-    private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
-    private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
-    private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
-    private static final String LINE_DELIMITER = "\\n";
-
-    private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
-        final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
-        return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
-                || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
-                || current.isAtLeast(new KVersion(5, 4, 98));
-    }
-
-    @Test
-    public void testIsUdpOffloadSupportedByKernel() throws Exception {
-        assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
-        assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
-        assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
-        assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
-        assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
-
-        assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
-        assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
-        assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
-        assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
-        assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
-
-        assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
-        assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
-        assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
-    }
-
-    private static void assumeKernelSupportBpfOffloadUdpV4() {
-        final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
-        assumeTrue("Kernel version " + kernelVersion + " doesn't support IPv4 UDP BPF offload",
-                isUdpOffloadSupportedByKernel(kernelVersion));
-    }
-
-    @Test
-    public void testKernelSupportBpfOffloadUdpV4() throws Exception {
-        assumeKernelSupportBpfOffloadUdpV4();
-    }
-
-    private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
-        final String dumpStr = runAsShell(DUMP, () ->
-                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short"));
-
-        // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
-        // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
-        // RRO to override the enabled default value. Get the tethering config via dumpsys.
-        // $ dumpsys tethering
-        //   mIsBpfEnabled: true
-        boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
-        if (!enabled) {
-            Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
-        }
-        return enabled;
-    }
-
-    @Test
-    public void testTetherConfigBpfOffloadEnabled() throws Exception {
-        assumeTrue(isTetherConfigBpfOffloadEnabled());
-    }
-
-    @NonNull
-    private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
-            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
-            throws Exception {
-        final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
-        final String rawMapStr = runAsShell(DUMP, () ->
-                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args));
-        final HashMap<K, V> map = new HashMap<>();
-
-        for (final String line : rawMapStr.split(LINE_DELIMITER)) {
-            final Pair<K, V> rule =
-                    BpfDump.fromBase64EncodedString(keyClass, valueClass, line.trim());
-            map.put(rule.first, rule.second);
-        }
-        return map;
-    }
-
-    @Nullable
-    private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
-            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
-            throws Exception {
-        for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
-            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
-            if (!map.isEmpty()) return map;
-
-            Thread.sleep(DUMP_POLLING_INTERVAL_MS);
-        }
-
-        fail("Cannot get rules after " + DUMP_POLLING_MAX_RETRY * DUMP_POLLING_INTERVAL_MS + "ms");
-        return null;
-    }
-
-    // Test network topology:
-    //
-    //         public network (rawip)                 private network
-    //                   |                 UE                |
-    // +------------+    V    +------------+------------+    V    +------------+
-    // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
-    // +------------+         +------------+------------+         +------------+
-    // remote ip              public ip                           private ip
-    // 8.8.8.8:443            <Upstream ip>:9876                  <TetheredDevice ip>:9876
-    //
-    private void runUdp4Test() throws Exception {
-        final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
-                toList(TEST_IP4_DNS));
-        final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
-
-        // TODO: remove the connectivity verification for upstream connected notification race.
-        // Because async upstream connected notification can't guarantee the tethering routing is
-        // ready to use. Need to test tethering connectivity before testing.
-        // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
-        // from upstream. That can guarantee that the routing is ready. Long term plan is that
-        // refactors upstream connected notification from async to sync.
-        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
-
-        final MacAddress srcMac = tethered.macAddr;
-        final MacAddress dstMac = tethered.routerMacAddr;
-        final InetAddress remoteIp = REMOTE_IP4_ADDR;
-        final InetAddress tetheringUpstreamIp = TEST_IP4_ADDR.getAddress();
-        final InetAddress clientIp = tethered.ipv4Addr;
-        sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
-        sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
-
-        // Send second UDP packet in original direction.
-        // The BPF coordinator only offloads the ASSURED conntrack entry. The "request + reply"
-        // packets can make status IPS_SEEN_REPLY to be set. Need one more packet to make
-        // conntrack status IPS_ASSURED_BIT to be set. Note the third packet needs to delay
-        // 2 seconds because kernel monitors a UDP connection which still alive after 2 seconds
-        // and apply ASSURED flag.
-        // See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
-        // nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
-        Thread.sleep(UDP_STREAM_TS_MS);
-        sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester, false /* is4To6 */);
-
-        // Give a slack time for handling conntrack event in user space.
-        Thread.sleep(UDP_STREAM_SLACK_MS);
-
-        // [1] Verify IPv4 upstream rule map.
-        final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
-                Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
-        assertNotNull(upstreamMap);
-        assertEquals(1, upstreamMap.size());
-
-        final Map.Entry<Tether4Key, Tether4Value> rule =
-                upstreamMap.entrySet().iterator().next();
-
-        final Tether4Key upstream4Key = rule.getKey();
-        assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
-        assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
-        assertEquals(LOCAL_PORT, upstream4Key.srcPort);
-        assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
-        assertEquals(REMOTE_PORT, upstream4Key.dstPort);
-
-        final Tether4Value upstream4Value = rule.getValue();
-        assertTrue(Arrays.equals(tetheringUpstreamIp.getAddress(),
-                InetAddress.getByAddress(upstream4Value.src46).getAddress()));
-        assertEquals(LOCAL_PORT, upstream4Value.srcPort);
-        assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
-                InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
-        assertEquals(REMOTE_PORT, upstream4Value.dstPort);
-
-        // [2] Verify stats map.
-        // Transmit packets on both direction for verifying stats. Because we only care the
-        // packet count in stats test, we just reuse the existing packets to increaes
-        // the packet count on both direction.
-
-        // Send packets on original direction.
-        for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
-            sendUploadPacketUdp(srcMac, dstMac, clientIp, remoteIp, tester,
-                    false /* is4To6 */);
-        }
-
-        // Send packets on reply direction.
-        for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
-            sendDownloadPacketUdp(remoteIp, tetheringUpstreamIp, tester, false /* is6To4 */);
-        }
-
-        // Dump stats map to verify.
-        final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
-                TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
-        assertNotNull(statsMap);
-        assertEquals(1, statsMap.size());
-
-        final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
-                statsMap.entrySet().iterator().next();
-
-        // TODO: verify the upstream index in TetherStatsKey.
-
-        final TetherStatsValue statsValue = stats.getValue();
-        assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
-        assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
-        assertEquals(0, statsValue.rxErrors);
-        assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
-        assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
-        assertEquals(0, statsValue.txErrors);
-    }
-
-    /**
-     * BPF offload IPv4 UDP tethering test. Verify that UDP tethered packets are offloaded by BPF.
-     * Minimum test requirement:
-     * 1. S+ device.
-     * 2. Tethering config enables tethering BPF offload.
-     * 3. Kernel supports IPv4 UDP BPF offload. See #isUdpOffloadSupportedByKernel.
-     *
-     * TODO: consider enabling the test even tethering config disables BPF offload. See b/238288883
-     */
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.R)
-    public void testTetherBpfOffloadUdpV4() throws Exception {
-        assumeTrue("Tethering config disabled BPF offload", isTetherConfigBpfOffloadEnabled());
-        assumeKernelSupportBpfOffloadUdpV4();
-
-        runUdp4Test();
-    }
-}
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
index ba6be66..3597a91 100644
--- a/Tethering/tests/privileged/Android.bp
+++ b/Tethering/tests/privileged/Android.bp
@@ -53,4 +53,5 @@
         "TetheringApiCurrentLib",
     ],
     compile_multilib: "both",
+    min_sdk_version: "30",
 }
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index d5d71bc..47aebe8 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -22,19 +22,24 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.net.MacAddress;
 import android.os.Build;
+import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.ArrayMap;
 
 import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.SingleWriterBpfMap;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -42,10 +47,18 @@
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import java.io.File;
 import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
@@ -56,11 +69,26 @@
     private static final int TEST_MAP_SIZE = 16;
     private static final String TETHER_DOWNSTREAM6_FS_PATH =
             "/sys/fs/bpf/tethering/map_test_tether_downstream6_map";
+    private static final String TETHER2_DOWNSTREAM6_FS_PATH =
+            "/sys/fs/bpf/tethering/map_test_tether2_downstream6_map";
+    private static final String TETHER3_DOWNSTREAM6_FS_PATH =
+            "/sys/fs/bpf/tethering/map_test_tether3_downstream6_map";
 
     private ArrayMap<TetherDownstream6Key, Tether6Value> mTestData;
 
     private BpfMap<TetherDownstream6Key, Tether6Value> mTestMap;
 
+    private final boolean mShouldTestSingleWriterMap;
+
+    @Parameterized.Parameters
+    public static Collection<Boolean> shouldTestSingleWriterMap() {
+        return Arrays.asList(true, false);
+    }
+
+    public BpfMapTest(boolean shouldTestSingleWriterMap) {
+        mShouldTestSingleWriterMap = shouldTestSingleWriterMap;
+    }
+
     @BeforeClass
     public static void setupOnce() {
         System.loadLibrary(getTetheringJniLibraryName());
@@ -82,11 +110,16 @@
         initTestMap();
     }
 
-    private void initTestMap() throws Exception {
-        mTestMap = new BpfMap<>(
-                TETHER_DOWNSTREAM6_FS_PATH,
-                TetherDownstream6Key.class, Tether6Value.class);
+    private BpfMap<TetherDownstream6Key, Tether6Value> openTestMap() throws Exception {
+        return mShouldTestSingleWriterMap
+                ? SingleWriterBpfMap.getSingleton(TETHER2_DOWNSTREAM6_FS_PATH,
+                        TetherDownstream6Key.class, Tether6Value.class)
+                : new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, TetherDownstream6Key.class,
+                        Tether6Value.class);
+    }
 
+    private void initTestMap() throws Exception {
+        mTestMap = openTestMap();
         mTestMap.forEach((key, value) -> {
             try {
                 assertTrue(mTestMap.deleteEntry(key));
@@ -125,7 +158,7 @@
                 assertEquals(OsConstants.EPERM, expected.errno);
             }
         }
-        try (BpfMap writeOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_WRONLY,
+        try (BpfMap writeOnlyMap = new BpfMap<>(TETHER3_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_WRONLY,
                 TetherDownstream6Key.class, Tether6Value.class)) {
             assertNotNull(writeOnlyMap);
             try {
@@ -357,6 +390,25 @@
     }
 
     @Test
+    public void testMapContentsCorrectOnOpen() throws Exception {
+        final BpfMap<TetherDownstream6Key, Tether6Value> map1, map2;
+
+        map1 = openTestMap();
+        map1.clear();
+        for (int i = 0; i < mTestData.size(); i++) {
+            map1.insertEntry(mTestData.keyAt(i), mTestData.valueAt(i));
+        }
+
+        // We can't close and reopen map1, because close does nothing. Open another map instead.
+        map2 = openTestMap();
+        for (int i = 0; i < mTestData.size(); i++) {
+            assertEquals(mTestData.valueAt(i), map2.getValue(mTestData.keyAt(i)));
+        }
+
+        map1.clear();
+    }
+
+    @Test
     public void testInsertOverflow() throws Exception {
         final ArrayMap<TetherDownstream6Key, Tether6Value> testData =
                 new ArrayMap<>();
@@ -396,8 +448,18 @@
         }
     }
 
-    private static int getNumOpenFds() {
-        return new File("/proc/" + Os.getpid() + "/fd").listFiles().length;
+    private static int getNumOpenBpfMapFds() throws Exception {
+        int numFds = 0;
+        File[] openFiles = new File("/proc/self/fd").listFiles();
+        for (int i = 0; i < openFiles.length; i++) {
+            final Path path = openFiles[i].toPath();
+            if (!Files.isSymbolicLink(path)) continue;
+            if ("anon_inode:bpf-map".equals(Files.readSymbolicLink(path).toString())) {
+                numFds++;
+            }
+        }
+        assertNotEquals("Couldn't find any BPF map fds opened by this process", 0, numFds);
+        return numFds;
     }
 
     @Test
@@ -406,7 +468,7 @@
         // cache, expect that the fd amount is not increased in the iterations.
         // See the comment of BpfMap#close.
         final int iterations = 1000;
-        final int before = getNumOpenFds();
+        final int before = getNumOpenBpfMapFds();
         for (int i = 0; i < iterations; i++) {
             try (BpfMap<TetherDownstream6Key, Tether6Value> map = new BpfMap<>(
                     TETHER_DOWNSTREAM6_FS_PATH,
@@ -414,15 +476,74 @@
                 // do nothing
             }
         }
-        final int after = getNumOpenFds();
+        final int after = getNumOpenBpfMapFds();
 
         // Check that the number of open fds is the same as before.
-        // If this exact match becomes flaky, we probably need to distinguish that fd is belong
-        // to "bpf-map".
-        // ex:
-        // $ adb shell ls -all /proc/16196/fd
-        // [..] network_stack 64 2022-07-26 22:01:02.300002956 +0800 749 -> anon_inode:bpf-map
-        // [..] network_stack 64 2022-07-26 22:01:02.188002956 +0800 75 -> anon_inode:[eventfd]
         assertEquals("Fd leak after " + iterations + " iterations: ", before, after);
     }
+
+    @Test
+    public void testNullKey() {
+        assertThrows(NullPointerException.class, () ->
+                mTestMap.insertOrReplaceEntry(null, mTestData.valueAt(0)));
+    }
+
+    private void runBenchmarkThread(BpfMap<TetherDownstream6Key, Tether6Value> map,
+            CompletableFuture<Integer> future, int runtimeMs) {
+        int numReads = 0;
+        final Random r = new Random();
+        final long start = SystemClock.elapsedRealtime();
+        final long stop = start + runtimeMs;
+        while (SystemClock.elapsedRealtime() < stop) {
+            try {
+                final Tether6Value v = map.getValue(mTestData.keyAt(r.nextInt(mTestData.size())));
+                assertNotNull(v);
+                numReads++;
+            } catch (Exception e) {
+                future.completeExceptionally(e);
+                return;
+            }
+        }
+        future.complete(numReads);
+    }
+
+    @Test
+    public void testSingleWriterCacheEffectiveness() throws Exception {
+        assumeTrue(mShouldTestSingleWriterMap);
+        // Benchmark parameters.
+        final int timeoutMs = 5_000;  // Only hit if threads don't complete.
+        final int benchmarkTimeMs = 2_000;
+        final int minReads = 50;
+        // Local testing on cuttlefish suggests that caching is ~10x faster.
+        // Only require 3x to reduce test flakiness.
+        final int expectedSpeedup = 3;
+
+        final BpfMap cachedMap = SingleWriterBpfMap.getSingleton(TETHER2_DOWNSTREAM6_FS_PATH,
+                TetherDownstream6Key.class, Tether6Value.class);
+        final BpfMap uncachedMap = new BpfMap(TETHER_DOWNSTREAM6_FS_PATH,
+                TetherDownstream6Key.class, Tether6Value.class);
+
+        // Ensure the maps are not empty.
+        for (int i = 0; i < mTestData.size(); i++) {
+            cachedMap.insertEntry(mTestData.keyAt(i), mTestData.valueAt(i));
+            uncachedMap.insertEntry(mTestData.keyAt(i), mTestData.valueAt(i));
+        }
+
+        final CompletableFuture<Integer> cachedResult = new CompletableFuture<>();
+        final CompletableFuture<Integer> uncachedResult = new CompletableFuture<>();
+
+        new Thread(() -> runBenchmarkThread(uncachedMap, uncachedResult, benchmarkTimeMs)).start();
+        new Thread(() -> runBenchmarkThread(cachedMap, cachedResult, benchmarkTimeMs)).start();
+
+        final int cached = cachedResult.get(timeoutMs, TimeUnit.MILLISECONDS);
+        final int uncached = uncachedResult.get(timeoutMs, TimeUnit.MILLISECONDS);
+
+        // Uncomment to see benchmark results.
+        // fail("Cached " + cached + ", uncached " + uncached + ": " + cached / uncached  +"x");
+
+        assertTrue("Less than " + minReads + "cached reads observed", cached > minReads);
+        assertTrue("Less than " + minReads + "uncached reads observed", uncached > minReads);
+        assertTrue("Cached map not at least " + expectedSpeedup + "x faster",
+                cached > expectedSpeedup * uncached);
+    }
 }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index a7064e8..177296a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -51,9 +51,9 @@
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -74,7 +74,6 @@
 import android.net.LinkProperties;
 import android.net.MacAddress;
 import android.net.RouteInfo;
-import android.net.RoutingCoordinatorManager;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.IDhcpEventCallbacks;
@@ -92,9 +91,9 @@
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.RoutingCoordinatorManager;
 import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.ip.IpNeighborMonitor;
 import com.android.networkstack.tethering.BpfCoordinator;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
 import com.android.networkstack.tethering.TetheringConfiguration;
@@ -174,11 +173,9 @@
     @Mock private IDhcpServer mDhcpServer;
     @Mock private DadProxy mDadProxy;
     @Mock private RouterAdvertisementDaemon mRaDaemon;
-    @Mock private IpNeighborMonitor mIpNeighborMonitor;
     @Mock private IpServer.Dependencies mDependencies;
     @Mock private PrivateAddressCoordinator mAddressCoordinator;
-    private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
-            new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
+    @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private TetheringMetrics mTetheringMetrics;
@@ -213,20 +210,17 @@
             mInterfaceConfiguration.prefixLength = BLUETOOTH_DHCP_PREFIX_LENGTH;
         }
 
-        doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(), any());
-
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
         when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
         when(mBpfCoordinator.isUsingBpfOffload()).thenReturn(usingBpfOffload);
         mIpServer = createIpServer(interfaceType);
-        verify(mIpNeighborMonitor).start();
         mIpServer.start();
 
         // Starting the state machine always puts us in a consistent state and notifies
         // the rest of the world that we've changed from an unknown to available state.
         mLooper.dispatchAll();
-        reset(mNetd, mCallback, mIpNeighborMonitor);
+        reset(mNetd, mCallback);
 
         when(mRaDaemon.start()).thenReturn(true);
     }
@@ -242,6 +236,7 @@
             throws Exception {
         initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
         dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+        verify(mBpfCoordinator).addIpServer(mIpServer);
         if (upstreamIface != null) {
             InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
             assertNotNull("missing upstream interface: " + upstreamIface, interfaceParams);
@@ -250,8 +245,12 @@
             lp.setLinkAddresses(upstreamAddresses);
             dispatchTetherConnectionChanged(upstreamIface, lp, 0);
             Set<IpPrefix> upstreamPrefixes = getTetherableIpv6Prefixes(lp.getLinkAddresses());
-            verify(mBpfCoordinator).updateAllIpv6Rules(
-                    mIpServer, TEST_IFACE_PARAMS, interfaceParams.index, upstreamPrefixes);
+            // One is called when handling CMD_TETHER_CONNECTION_CHANGED and the other one is called
+            // when upstream's LinkProperties is updated (updateUpstreamIPv6LinkProperties)
+            verify(mBpfCoordinator, times(2)).maybeAddUpstreamToLookupTable(
+                    interfaceParams.index, upstreamIface);
+            verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                    mIpServer, interfaceParams.index, upstreamPrefixes);
         }
         reset(mNetd, mBpfCoordinator, mCallback, mAddressCoordinator);
         when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
@@ -281,24 +280,6 @@
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
 
-        // Simulate the behavior of RoutingCoordinator
-        if (null != mRoutingCoordinatorManager.value) {
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.tetherAddForward(fromIface, toIface);
-                mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
-                mNetd.tetherRemoveForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
-        }
-
         setUpDhcpServer();
     }
 
@@ -314,8 +295,6 @@
 
     @Test
     public void startsOutAvailable() throws Exception {
-        when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
-                .thenReturn(mIpNeighborMonitor);
         mIpServer = createIpServer(TETHERING_BLUETOOTH);
         mIpServer.start();
         mLooper.dispatchAll();
@@ -458,107 +437,116 @@
         // Telling the state machine about its upstream interface triggers
         // a little more configuration.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX,
                 UPSTREAM_IFACE);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
-        verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+        verifyNoMoreInteractions(mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
     public void handlesChangingUpstream() throws Exception {
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
 
+        clearInvocations(mBpfCoordinator, mRoutingCoordinatorManager);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
-        verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+        verifyNoMoreInteractions(mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
     public void handlesChangingUpstreamNatFailure() throws Exception {
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
 
-        doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
-        // tetherAddForward.
+        // addInterfaceForward.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
     }
 
     @Test
     public void handlesChangingUpstreamInterfaceForwardingFailure() throws Exception {
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
 
-        doThrow(RemoteException.class).when(mNetd).ipfwdAddInterfaceForward(
-                IFACE_NAME, UPSTREAM_IFACE2);
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
         // ipfwdAddInterfaceForward.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
     }
 
     @Test
     public void canUnrequestTetheringWithUpstream() throws Exception {
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
 
+        clearInvocations(
+                mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
         dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
-        InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+        InOrder inOrder =
+                inOrder(
+                        mNetd,
+                        mCallback,
+                        mAddressCoordinator,
+                        mBpfCoordinator,
+                        mRoutingCoordinatorManager);
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         // When tethering stops, upstream interface is set to zero and thus clearing all upstream
         // rules. Downstream rules are needed to be cleared explicitly by calling
         // BpfCoordinator#clearAllIpv6Rules in TetheredState#exit.
@@ -570,12 +558,13 @@
                 argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
         inOrder.verify(mAddressCoordinator).releaseDownstream(any());
         inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer);
-        inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
+        inOrder.verify(mBpfCoordinator).removeIpServer(mIpServer);
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), any(LinkProperties.class));
-        verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+        verifyNoMoreInteractions(
+                mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
@@ -627,10 +616,14 @@
     public void shouldTearDownUsbOnUpstreamError() throws Exception {
         initTetheredStateMachine(TETHERING_USB, null);
 
-        doThrow(RemoteException.class).when(mNetd).tetherAddForward(anyString(), anyString());
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(anyString(), anyString());
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
-        usbTeardownOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
+        InOrder usbTeardownOrder = inOrder(mNetd, mCallback, mRoutingCoordinatorManager);
+        usbTeardownOrder
+                .verify(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg(
                 argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
@@ -765,8 +758,8 @@
         lp.setInterfaceName(UPSTREAM_IFACE2);
         lp.setLinkAddresses(UPSTREAM_ADDRESSES);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
         reset(mBpfCoordinator);
 
         // Upstream link addresses change result in updating the rules.
@@ -774,8 +767,8 @@
         lp2.setInterfaceName(UPSTREAM_IFACE2);
         lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, -1);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
         reset(mBpfCoordinator);
 
         // When the upstream is lost, rules are removed.
@@ -784,53 +777,54 @@
         // - processMessage CMD_TETHER_CONNECTION_CHANGED for the upstream is lost.
         // - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost.
         // See dispatchTetherConnectionChanged.
-        verify(mBpfCoordinator, times(2)).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        verify(mBpfCoordinator, times(2)).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         reset(mBpfCoordinator);
 
         // If the upstream is IPv4-only, no rules are added.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        verify(mBpfCoordinator, never()).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        verify(mBpfCoordinator, never()).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         reset(mBpfCoordinator);
 
         // Rules are added again once upstream IPv6 connectivity is available.
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
         reset(mBpfCoordinator);
 
         // If upstream IPv6 connectivity is lost, rules are removed.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         reset(mBpfCoordinator);
 
         // When upstream IPv6 connectivity comes back, rules are added.
         lp.setInterfaceName(UPSTREAM_IFACE);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
         reset(mBpfCoordinator);
 
         // When the downstream interface goes down, rules are removed.
         mIpServer.stop();
         mLooper.dispatchAll();
         verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
-        verify(mBpfCoordinator).updateAllIpv6Rules(
-                mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        verify(mBpfCoordinator).removeIpServer(mIpServer);
+        verify(mBpfCoordinator).updateIpv6UpstreamInterface(
+                mIpServer, NO_UPSTREAM, NO_PREFIXES);
         reset(mBpfCoordinator);
     }
 
     @Test
-    public void stopNeighborMonitoringWhenInterfaceDown() throws Exception {
+    public void removeIpServerWhenInterfaceDown() throws Exception {
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
                 false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
 
         mIpServer.stop();
         mLooper.dispatchAll();
-        verify(mIpNeighborMonitor).stop();
+        verify(mBpfCoordinator).removeIpServer(mIpServer);
     }
 
     private LinkProperties buildIpv6OnlyLinkProperties(final String iface) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 47ecf58..e54a7e0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -25,8 +25,6 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStats.UID_TETHERING;
-import static android.net.TetheringManager.TETHERING_WIFI;
-import static android.net.ip.IpServer.STATE_TETHERED;
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
@@ -79,9 +77,7 @@
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
@@ -100,11 +96,9 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkStats;
-import android.net.RoutingCoordinatorManager;
 import android.net.TetherOffloadRuleParcel;
 import android.net.TetherStatsParcel;
 import android.net.ip.IpServer;
-import android.net.ip.RouterAdvertisementDaemon;
 import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
@@ -119,12 +113,10 @@
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
-import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.bpf.Tether4Key;
@@ -143,8 +135,6 @@
 import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
-import com.android.networkstack.tethering.metrics.TetheringMetrics;
-import com.android.networkstack.tethering.util.InterfaceSet;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -209,11 +199,6 @@
     private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
     private static final MacAddress MAC_NULL = MacAddress.fromString("00:00:00:00:00:00");
 
-    private static final LinkAddress UPSTREAM_ADDRESS = new LinkAddress("2001:db8:0:1234::168/64");
-    private static final LinkAddress UPSTREAM_ADDRESS2 = new LinkAddress("2001:db8:0:abcd::168/64");
-    private static final Set<LinkAddress> UPSTREAM_ADDRESSES = Set.of(UPSTREAM_ADDRESS);
-    private static final Set<LinkAddress> UPSTREAM_ADDRESSES2 =
-            Set.of(UPSTREAM_ADDRESS, UPSTREAM_ADDRESS2);
     private static final IpPrefix UPSTREAM_PREFIX = new IpPrefix("2001:db8:0:1234::/64");
     private static final IpPrefix UPSTREAM_PREFIX2 = new IpPrefix("2001:db8:0:abcd::/64");
     private static final Set<IpPrefix> UPSTREAM_PREFIXES = Set.of(UPSTREAM_PREFIX);
@@ -449,13 +434,6 @@
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
     @Mock private IpNeighborMonitor mIpNeighborMonitor;
-    @Mock private RouterAdvertisementDaemon mRaDaemon;
-    @Mock private IpServer.Dependencies mIpServerDeps;
-    @Mock private IpServer.Callback mIpServerCallback;
-    @Mock private PrivateAddressCoordinator mAddressCoordinator;
-    private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
-            new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
-    @Mock private TetheringMetrics mTetheringMetrics;
 
     // Late init since methods must be called by the thread that created this object.
     private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
@@ -573,24 +551,8 @@
     @Before public void setUp() {
         MockitoAnnotations.initMocks(this);
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
-
-        // Simulate the behavior of RoutingCoordinator
-        if (null != mRoutingCoordinatorManager.value) {
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.tetherAddForward(fromIface, toIface);
-                mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
-                mNetd.tetherRemoveForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
-        }
+        when(mIpServer.getInterfaceParams()).thenReturn(DOWNSTREAM_IFACE_PARAMS);
+        when(mIpServer2.getInterfaceParams()).thenReturn(DOWNSTREAM_IFACE_PARAMS2);
     }
 
     private void waitForIdle() {
@@ -603,70 +565,39 @@
         when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
     }
 
-    @NonNull
-    private IpServer makeAndStartIpServer(String interfaceName, BpfCoordinator bpfCoordinator)
-            throws Exception {
-        final LinkAddress testAddress = new LinkAddress("192.168.42.5/24");
-        when(mIpServerDeps.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
-        when(mIpServerDeps.getInterfaceParams(DOWNSTREAM_IFACE)).thenReturn(
-                DOWNSTREAM_IFACE_PARAMS);
-        when(mIpServerDeps.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
-        when(mIpServerDeps.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
-        when(mIpServerDeps.getInterfaceParams(IPSEC_IFACE)).thenReturn(IPSEC_IFACE_PARAMS);
-        when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
-                anyBoolean())).thenReturn(testAddress);
-        when(mRaDaemon.start()).thenReturn(true);
-        ArgumentCaptor<NeighborEventConsumer> neighborEventCaptor =
-                ArgumentCaptor.forClass(NeighborEventConsumer.class);
-        doReturn(mIpNeighborMonitor).when(mIpServerDeps).getIpNeighborMonitor(any(), any(),
-                neighborEventCaptor.capture());
-        final IpServer ipServer = new IpServer(
-                interfaceName, mHandler, TETHERING_WIFI, new SharedLog("test"), mNetd,
-                bpfCoordinator, mRoutingCoordinatorManager, mIpServerCallback, mTetherConfig,
-                mAddressCoordinator, mTetheringMetrics, mIpServerDeps);
-        ipServer.start();
-        ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
-        mTestLooper.dispatchAll();
-
-        LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(UPSTREAM_IFACE);
-        lp.setLinkAddresses(UPSTREAM_ADDRESSES);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, 0);
-
-        mNeighborEventConsumer = neighborEventCaptor.getValue();
-        return ipServer;
-    }
-
-    private void dispatchTetherConnectionChanged(IpServer ipServer, String upstreamIface,
-            LinkProperties v6lp, int ttlAdjustment) {
-        dispatchTetherConnectionChanged(ipServer, upstreamIface);
-        ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, ttlAdjustment, 0, v6lp);
-        mTestLooper.dispatchAll();
-    }
-
-    private void dispatchTetherConnectionChanged(IpServer ipServer, String upstreamIface) {
-        final InterfaceSet ifs = (upstreamIface != null) ? new InterfaceSet(upstreamIface) : null;
-        ipServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifs);
-        mTestLooper.dispatchAll();
+    private void dispatchIpv6UpstreamChanged(BpfCoordinator bpfCoordinator, IpServer ipServer,
+            int upstreamIfindex, String upstreamIface, Set<IpPrefix> upstreamPrefixes) {
+        bpfCoordinator.maybeAddUpstreamToLookupTable(upstreamIfindex, upstreamIface);
+        bpfCoordinator.updateIpv6UpstreamInterface(ipServer, upstreamIfindex, upstreamPrefixes);
+        when(ipServer.getIpv6UpstreamIfindex()).thenReturn(upstreamIfindex);
+        when(ipServer.getIpv6UpstreamPrefixes()).thenReturn(upstreamPrefixes);
     }
 
     private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
         mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr,
                 nudState, mac));
-        mTestLooper.dispatchAll();
     }
 
     private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
         mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr,
                 nudState, mac));
-        mTestLooper.dispatchAll();
     }
 
     @NonNull
     private BpfCoordinator makeBpfCoordinator() throws Exception {
+        return makeBpfCoordinator(true /* addDefaultIpServer */);
+    }
+
+    @NonNull
+    private BpfCoordinator makeBpfCoordinator(boolean addDefaultIpServer) throws Exception {
         // mStatsManager will be invoked twice if BpfCoordinator is created the second time.
         clearInvocations(mStatsManager);
+        ArgumentCaptor<NeighborEventConsumer> neighborCaptor =
+                ArgumentCaptor.forClass(NeighborEventConsumer.class);
+        doReturn(mIpNeighborMonitor).when(mDeps).getIpNeighborMonitor(neighborCaptor.capture());
         final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
+        mNeighborEventConsumer = neighborCaptor.getValue();
+        assertNotNull(mNeighborEventConsumer);
 
         mConsumer = coordinator.getBpfConntrackEventConsumerForTesting();
         mTetherClients = coordinator.getTetherClientsForTesting();
@@ -681,6 +612,10 @@
         mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
         mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
 
+        if (addDefaultIpServer) {
+            coordinator.addIpServer(mIpServer);
+        }
+
         return coordinator;
     }
 
@@ -1008,7 +943,6 @@
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
-        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // InOrder is required because mBpfStatsMap may be accessed by both
         // BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats.
@@ -1020,19 +954,18 @@
                 mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
         final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(
                 mobileIfIndex, NEIGH_A, MAC_A);
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         verifyAddUpstreamRule(inOrder, upstreamRule);
-        coordinator.addIpv6DownstreamRule(mIpServer, downstreamRule);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
         verifyAddDownstreamRule(inOrder, downstreamRule);
 
         // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         verifyRemoveDownstreamRule(inOrder, downstreamRule);
         verifyRemoveUpstreamRule(inOrder, upstreamRule);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
@@ -1056,7 +989,6 @@
 
         doReturn(usingApiS).when(mDeps).isAtLeastS();
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
@@ -1092,7 +1024,6 @@
         setupFunctioningNetdInterface();
 
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String wlanIface = "wlan0";
         final Integer wlanIfIndex = 100;
@@ -1148,7 +1079,7 @@
         // [3] Stop coordinator.
         // Shutdown the coordinator and clear the invocation history, especially the
         // tetherOffloadGetStats() calls.
-        coordinator.stopPolling();
+        coordinator.removeIpServer(mIpServer);
         clearStatsInvocations();
 
         // Verify the polling update thread stopped.
@@ -1162,7 +1093,6 @@
         setupFunctioningNetdInterface();
 
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         final String mobileIface = "rmnet_data0";
         final Integer mobileIfIndex = 100;
@@ -1350,7 +1280,6 @@
 
         final String mobileIface = "rmnet_data0";
         final int mobileIfIndex = 100;
-        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // [1] Default limit.
         // Set the unlimited quota as default if the service has never applied a data limit for a
@@ -1358,8 +1287,8 @@
         final Ipv6UpstreamRule rule = buildTestUpstreamRule(
                 mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
         final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         verifyAddUpstreamRule(inOrder, rule);
@@ -1395,7 +1324,6 @@
 
         final String mobileIface = "rmnet_data0";
         final int mobileIfIndex = 100;
-        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         // Applying a data limit to the current upstream does not take any immediate action.
         // The data limit could be only set on an upstream which has rules.
@@ -1408,31 +1336,30 @@
         // Adding the first rule on current upstream immediately sends the quota to BPF.
         final Ipv6UpstreamRule ruleA = buildTestUpstreamRule(
                 mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
         verifyAddUpstreamRule(inOrder, ruleA);
         inOrder.verifyNoMoreInteractions();
 
         // Adding the second rule on current upstream does not send the quota to BPF.
+        coordinator.addIpServer(mIpServer2);
         final Ipv6UpstreamRule ruleB = buildTestUpstreamRule(
                 mobileIfIndex, DOWNSTREAM_IFINDEX2, UPSTREAM_PREFIX, DOWNSTREAM_MAC2);
-        coordinator.updateAllIpv6Rules(
-                mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer2, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES);
         verifyAddUpstreamRule(inOrder, ruleB);
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Removing the second rule on current upstream does not send the quota to BPF.
-        coordinator.updateAllIpv6Rules(
-                mIpServer2, DOWNSTREAM_IFACE_PARAMS2, NO_UPSTREAM, NO_PREFIXES);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer2, NO_UPSTREAM, null, NO_PREFIXES);
         verifyRemoveUpstreamRule(inOrder, ruleB);
         verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
 
         // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
         updateStatsEntryForTetherOffloadGetAndClearStats(
                 buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         verifyRemoveUpstreamRule(inOrder, ruleA);
         verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
         inOrder.verifyNoMoreInteractions();
@@ -1448,8 +1375,6 @@
         final String mobileIface = "rmnet_data0";
         final Integer ethIfIndex = 100;
         final Integer mobileIfIndex = 101;
-        coordinator.maybeAddUpstreamToLookupTable(ethIfIndex, ethIface);
-        coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
 
         final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap,
                 mBpfStatsMap);
@@ -1470,14 +1395,14 @@
         final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
                 ethIfIndex, NEIGH_B, MAC_B);
 
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, ethIfIndex, ethIface, UPSTREAM_PREFIXES);
         verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
                 true /* isInit */);
         verifyAddUpstreamRule(inOrder, ethernetUpstreamRule);
-        coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleA);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
         verifyAddDownstreamRule(inOrder, ethernetRuleA);
-        coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleB);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_B, NUD_REACHABLE, MAC_B);
         verifyAddDownstreamRule(inOrder, ethernetRuleB);
 
         // [2] Update the existing rules from Ethernet to cellular.
@@ -1494,8 +1419,8 @@
 
         // Update the existing rules for upstream changes. The rules are removed and re-added one
         // by one for updating upstream interface index and prefixes by #tetherOffloadRuleUpdate.
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES2);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, mobileIfIndex, mobileIface, UPSTREAM_PREFIXES2);
         verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
         verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
         verifyRemoveUpstreamRule(inOrder, ethernetUpstreamRule);
@@ -1532,7 +1457,6 @@
         // #makeBpfCoordinator for testing.
         // See #testBpfDisabledbyNoBpfDownstream6Map.
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        coordinator.startPolling();
 
         // The tether stats polling task should not be scheduled.
         mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
@@ -1549,7 +1473,7 @@
         final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
         final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
         final Ipv6DownstreamRule rule = buildTestDownstreamRule(ifIndex, neigh, mac);
-        coordinator.addIpv6DownstreamRule(mIpServer, rule);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, neigh, NUD_REACHABLE, mac);
         verifyNeverAddDownstreamRule();
         LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
                 coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1561,7 +1485,7 @@
         rules = new LinkedHashMap<Inet6Address, Ipv6DownstreamRule>();
         rules.put(rule.address, rule);
         coordinator.getIpv6DownstreamRulesForTesting().put(mIpServer, rules);
-        coordinator.removeIpv6DownstreamRule(mIpServer, rule);
+        recvNewNeigh(DOWNSTREAM_IFINDEX, neigh, NUD_STALE, mac);
         verifyNeverRemoveDownstreamRule();
         rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
         assertNotNull(rules);
@@ -1575,8 +1499,8 @@
         assertEquals(1, rules.size());
 
         // The rule can't be updated.
-        coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS,
-                rule.upstreamIfindex + 1 /* new */, UPSTREAM_PREFIXES);
+        coordinator.updateIpv6UpstreamInterface(mIpServer, rule.upstreamIfindex + 1 /* new */,
+                UPSTREAM_PREFIXES);
         verifyNeverRemoveDownstreamRule();
         verifyNeverAddDownstreamRule();
         rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1753,18 +1677,14 @@
         final BpfCoordinator coordinator = makeBpfCoordinator();
 
         // [1] The default polling interval.
-        coordinator.startPolling();
         assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval());
-        coordinator.stopPolling();
 
         // [2] Expect the invalid polling interval isn't applied. The valid range of interval is
         // DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long.
         for (final int interval
                 : new int[] {0, 100, DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS - 1}) {
             when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval);
-            coordinator.startPolling();
             assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval());
-            coordinator.stopPolling();
         }
 
         // [3] Set a specific polling interval which is larger than default value.
@@ -1772,7 +1692,6 @@
         // approximation is used to verify the scheduled time of the polling thread.
         final int pollingInterval = 100_000;
         when(mTetherConfig.getOffloadPollInterval()).thenReturn(pollingInterval);
-        coordinator.startPolling();
 
         // Expect the specific polling interval to be applied.
         assertEquals(pollingInterval, coordinator.getPollingInterval());
@@ -1800,19 +1719,19 @@
     public void testStartStopConntrackMonitoring() throws Exception {
         setupFunctioningNetdInterface();
 
-        final BpfCoordinator coordinator = makeBpfCoordinator();
+        final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
 
         // [1] Don't stop monitoring if it has never started.
-        coordinator.stopMonitoring(mIpServer);
+        coordinator.removeIpServer(mIpServer);
         verify(mConntrackMonitor, never()).stop();
 
         // [2] Start monitoring.
-        coordinator.startMonitoring(mIpServer);
+        coordinator.addIpServer(mIpServer);
         verify(mConntrackMonitor).start();
         clearInvocations(mConntrackMonitor);
 
         // [3] Stop monitoring.
-        coordinator.stopMonitoring(mIpServer);
+        coordinator.removeIpServer(mIpServer);
         verify(mConntrackMonitor).stop();
     }
 
@@ -1823,12 +1742,12 @@
     public void testStartStopConntrackMonitoring_R() throws Exception {
         setupFunctioningNetdInterface();
 
-        final BpfCoordinator coordinator = makeBpfCoordinator();
+        final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
 
-        coordinator.startMonitoring(mIpServer);
+        coordinator.addIpServer(mIpServer);
         verify(mConntrackMonitor, never()).start();
 
-        coordinator.stopMonitoring(mIpServer);
+        coordinator.removeIpServer(mIpServer);
         verify(mConntrackMonitor, never()).stop();
     }
 
@@ -1837,23 +1756,23 @@
     public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception {
         setupFunctioningNetdInterface();
 
-        final BpfCoordinator coordinator = makeBpfCoordinator();
+        final BpfCoordinator coordinator = makeBpfCoordinator(false /* addDefaultIpServer */);
 
         // [1] Start monitoring at the first IpServer adding.
-        coordinator.startMonitoring(mIpServer);
+        coordinator.addIpServer(mIpServer);
         verify(mConntrackMonitor).start();
         clearInvocations(mConntrackMonitor);
 
         // [2] Don't start monitoring at the second IpServer adding.
-        coordinator.startMonitoring(mIpServer2);
+        coordinator.addIpServer(mIpServer2);
         verify(mConntrackMonitor, never()).start();
 
         // [3] Don't stop monitoring if any downstream interface exists.
-        coordinator.stopMonitoring(mIpServer2);
+        coordinator.removeIpServer(mIpServer2);
         verify(mConntrackMonitor, never()).stop();
 
         // [4] Stop monitoring if no downstream exists.
-        coordinator.stopMonitoring(mIpServer);
+        coordinator.removeIpServer(mIpServer);
         verify(mConntrackMonitor).stop();
     }
 
@@ -2016,9 +1935,8 @@
     public void testAddDevMapRule6() throws Exception {
         final BpfCoordinator coordinator = makeBpfCoordinator();
 
-        coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
-        coordinator.updateAllIpv6Rules(
-                mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
                 eq(new TetherDevValue(UPSTREAM_IFINDEX)));
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
@@ -2027,8 +1945,9 @@
 
         // Adding the second downstream, only the second downstream ifindex is added to DevMap,
         // the existing upstream ifindex won't be added again.
-        coordinator.updateAllIpv6Rules(
-                mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+        coordinator.addIpServer(mIpServer2);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer2, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX2)),
                 eq(new TetherDevValue(DOWNSTREAM_IFINDEX2)));
         verify(mBpfDevMap, never()).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
@@ -2087,7 +2006,6 @@
                 .startMocking();
         try {
             final BpfCoordinator coordinator = makeBpfCoordinator();
-            coordinator.startPolling();
             bpfMap.insertEntry(tcpKey, tcpValue);
             bpfMap.insertEntry(udpKey, udpValue);
 
@@ -2116,7 +2034,7 @@
             ExtendedMockito.clearInvocations(staticMockMarker(NetlinkUtils.class));
 
             // [3] Don't refresh conntrack timeout if polling stopped.
-            coordinator.stopPolling();
+            coordinator.removeIpServer(mIpServer);
             mTestLooper.moveTimeForward(CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
             waitForIdle();
             ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkUtils.class));
@@ -2855,8 +2773,9 @@
         final int myIfindex = DOWNSTREAM_IFINDEX;
         final int notMyIfindex = myIfindex - 1;
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
 
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         resetNetdAndBpfMaps();
         verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
 
@@ -2905,10 +2824,8 @@
         resetNetdAndBpfMaps();
 
         InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-        LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName(UPSTREAM_IFACE2);
-        lp.setLinkAddresses(UPSTREAM_ADDRESSES);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp, -1);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES);
         final Ipv6DownstreamRule ruleA2 = buildTestDownstreamRule(
                 UPSTREAM_IFINDEX2, NEIGH_A, MAC_A);
         final Ipv6DownstreamRule ruleB2 = buildTestDownstreamRule(
@@ -2922,11 +2839,9 @@
         verifyNoUpstreamIpv6ForwardingChange(inOrder);
         resetNetdAndBpfMaps();
 
-        // Upstream link addresses change result in updating the rules.
-        LinkProperties lp2 = new LinkProperties();
-        lp2.setInterfaceName(UPSTREAM_IFACE2);
-        lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp2, -1);
+        // Upstream prefixes change result in updating the rules.
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES2);
         verifyRemoveDownstreamRule(inOrder, ruleA2);
         verifyRemoveDownstreamRule(inOrder, ruleB2);
         verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
@@ -2936,7 +2851,7 @@
         resetNetdAndBpfMaps();
 
         // When the upstream is lost, rules are removed.
-        dispatchTetherConnectionChanged(ipServer, null, null, 0);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES2);
         verifyRemoveDownstreamRule(ruleA2);
         verifyRemoveDownstreamRule(ruleB2);
@@ -2947,7 +2862,7 @@
         resetNetdAndBpfMaps();
 
         // If the upstream is IPv4-only, no IPv6 rules are added to BPF map.
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         resetNetdAndBpfMaps();
         recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
         verifyNoUpstreamIpv6ForwardingChange(null);
@@ -2957,8 +2872,8 @@
 
         // Rules can be added again once upstream IPv6 connectivity is available. The existing rules
         // with an upstream of NO_UPSTREAM are reapplied.
-        lp.setInterfaceName(UPSTREAM_IFACE);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, -1);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
         verifyAddDownstreamRule(ruleA);
         recvNewNeigh(myIfindex, NEIGH_B, NUD_REACHABLE, MAC_B);
@@ -2966,23 +2881,27 @@
 
         // If upstream IPv6 connectivity is lost, rules are removed.
         resetNetdAndBpfMaps();
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, null, 0);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
         verifyRemoveDownstreamRule(ruleA);
         verifyRemoveDownstreamRule(ruleB);
         verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
 
         // When upstream IPv6 connectivity comes back, upstream rules are added and downstream rules
         // are reapplied.
-        lp.setInterfaceName(UPSTREAM_IFACE);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, -1);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
         verifyAddDownstreamRule(ruleA);
         verifyAddDownstreamRule(ruleB);
         resetNetdAndBpfMaps();
 
         // When the downstream interface goes down, rules are removed.
-        ipServer.stop();
-        mTestLooper.dispatchAll();
+        // Simulate receiving CMD_INTERFACE_DOWN in the BaseServingState of IpServer.
+        reset(mIpNeighborMonitor);
+        dispatchIpv6UpstreamChanged(coordinator, mIpServer, NO_UPSTREAM, null, NO_PREFIXES);
+        coordinator.tetherOffloadClientClear(mIpServer);
+        coordinator.removeIpServer(mIpServer);
+
         verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
         verifyRemoveDownstreamRule(ruleA);
         verifyRemoveDownstreamRule(ruleB);
@@ -3004,7 +2923,8 @@
         // [1] Enable BPF offload.
         // A neighbor that is added or deleted causes the rule to be added or removed.
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
         resetNetdAndBpfMaps();
 
         recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
@@ -3019,10 +2939,8 @@
         resetNetdAndBpfMaps();
 
         // Upstream IPv6 connectivity change causes upstream rules change.
-        LinkProperties lp2 = new LinkProperties();
-        lp2.setInterfaceName(UPSTREAM_IFACE2);
-        lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
-        dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp2, 0);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, UPSTREAM_PREFIXES2);
         verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
         resetNetdAndBpfMaps();
 
@@ -3030,7 +2948,6 @@
         // A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
         final BpfCoordinator coordinator2 = makeBpfCoordinator();
-        final IpServer ipServer2 = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator2);
         verifyNoUpstreamIpv6ForwardingChange(null);
         resetNetdAndBpfMaps();
 
@@ -3043,7 +2960,8 @@
         resetNetdAndBpfMaps();
 
         // Upstream IPv6 connectivity change doesn't cause the rule to be added or removed.
-        dispatchTetherConnectionChanged(ipServer2, UPSTREAM_IFACE2, lp2, 0);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer2, UPSTREAM_IFINDEX2, UPSTREAM_IFACE2, NO_PREFIXES);
         verifyNoUpstreamIpv6ForwardingChange(null);
         verifyNeverRemoveDownstreamRule();
         resetNetdAndBpfMaps();
@@ -3053,7 +2971,6 @@
     public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
 
         // IP neighbor monitor doesn't start if BPF offload is disabled.
         verify(mIpNeighborMonitor, never()).start();
@@ -3062,15 +2979,10 @@
     @Test
     public void testSkipVirtualNetworkInBpf() throws Exception {
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
-        final LinkProperties v6Only = new LinkProperties();
-        v6Only.setInterfaceName(IPSEC_IFACE);
-        v6Only.setLinkAddresses(UPSTREAM_ADDRESSES);
 
         resetNetdAndBpfMaps();
-        dispatchTetherConnectionChanged(ipServer, IPSEC_IFACE, v6Only, 0);
-        verify(mNetd).tetherAddForward(DOWNSTREAM_IFACE, IPSEC_IFACE);
-        verify(mNetd).ipfwdAddInterfaceForward(DOWNSTREAM_IFACE, IPSEC_IFACE);
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, IPSEC_IFINDEX, IPSEC_IFACE, UPSTREAM_PREFIXES);
         verifyNeverAddUpstreamRule();
 
         recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
@@ -3080,7 +2992,6 @@
     @Test
     public void addRemoveTetherClient() throws Exception {
         final BpfCoordinator coordinator = makeBpfCoordinator();
-        final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
         final int myIfindex = DOWNSTREAM_IFINDEX;
         final int notMyIfindex = myIfindex - 1;
 
@@ -3089,33 +3000,36 @@
         final InetAddress neighLL = InetAddresses.parseNumericAddress("169.254.0.1");
         final InetAddress neighMC = InetAddresses.parseNumericAddress("224.0.0.1");
 
+        dispatchIpv6UpstreamChanged(
+                coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES);
+
         // Events on other interfaces are ignored.
         recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, MAC_A);
-        assertNull(mTetherClients.get(ipServer));
+        assertNull(mTetherClients.get(mIpServer));
 
         // Events on this interface are received and sent to BpfCoordinator.
         recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, MAC_A);
-        assertClientInfoExists(ipServer,
+        assertClientInfoExists(mIpServer,
                 new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighA, MAC_A));
 
         recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, MAC_B);
-        assertClientInfoExists(ipServer,
+        assertClientInfoExists(mIpServer,
                 new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighB, MAC_B));
 
         // Link-local and multicast neighbors are ignored.
         recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, MAC_A);
-        assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighLL);
+        assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighLL);
         recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, MAC_A);
-        assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighMC);
+        assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighMC);
 
         // A neighbor that is no longer valid causes the client to be removed.
         // NUD_FAILED events do not have a MAC address.
         recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
-        assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighA);
+        assertClientInfoDoesNotExist(mIpServer, (Inet4Address) neighA);
 
         // A neighbor that is deleted causes the client to be removed.
         recvDelNeigh(myIfindex, neighB, NUD_STALE, MAC_B);
         // When last client information is deleted, IpServer will be removed from mTetherClients
-        assertNull(mTetherClients.get(ipServer));
+        assertNull(mTetherClients.get(mIpServer));
     }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index da81bda..c0d7ad4 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -45,6 +45,7 @@
 import android.net.ITetheringConnector;
 import android.net.ITetheringEventCallback;
 import android.net.TetheringManager;
+import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringRequestParcel;
 import android.net.ip.IpServer;
 import android.os.Bundle;
@@ -311,7 +312,8 @@
                 result);
         verify(mTethering).isTetheringSupported();
         verify(mTethering).isTetheringAllowed();
-        verify(mTethering).startTethering(eq(request), eq(TEST_CALLER_PKG), eq(result));
+        verify(mTethering).startTethering(
+                eq(new TetheringRequest(request)), eq(TEST_CALLER_PKG), eq(result));
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 750bfce..6ba5d48 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -142,7 +142,6 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.RouteInfo;
-import android.net.RoutingCoordinatorManager;
 import android.net.TetherStatesParcel;
 import android.net.TetheredClient;
 import android.net.TetheredClient.AddressInfo;
@@ -150,7 +149,7 @@
 import android.net.TetheringConfigurationParcel;
 import android.net.TetheringInterface;
 import android.net.TetheringManager;
-import android.net.TetheringRequestParcel;
+import android.net.TetheringManager.TetheringRequest;
 import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
@@ -191,7 +190,7 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorManager;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.ip.IpNeighborMonitor;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -292,6 +291,7 @@
     @Mock private BluetoothPanShim mBluetoothPanShim;
     @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
     @Mock private TetheringMetrics mTetheringMetrics;
+    @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -484,10 +484,10 @@
             return mEntitleMgr;
         }
 
-        @Nullable
         @Override
-        public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(final Context context) {
-            return new LateSdk<>(null);
+        public RoutingCoordinatorManager getRoutingCoordinator(final Context context,
+                SharedLog log) {
+            return mRoutingCoordinatorManager;
         }
 
         @Override
@@ -498,7 +498,7 @@
         }
 
         @Override
-        public INetd getINetd(Context context) {
+        public INetd getINetd(Context context, SharedLog log) {
             return mNetd;
         }
 
@@ -528,7 +528,7 @@
         }
 
         @Override
-        public TetheringMetrics makeTetheringMetrics() {
+        public TetheringMetrics makeTetheringMetrics(Context ctx) {
             return mTetheringMetrics;
         }
 
@@ -743,22 +743,21 @@
         doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
     }
 
-    private TetheringRequestParcel createTetheringRequestParcel(final int type) {
-        return createTetheringRequestParcel(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
+    private TetheringRequest createTetheringRequest(final int type) {
+        return createTetheringRequest(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
     }
 
-    private TetheringRequestParcel createTetheringRequestParcel(final int type,
-            final LinkAddress serverAddr, final LinkAddress clientAddr, final boolean exempt,
-            final int scope) {
-        final TetheringRequestParcel request = new TetheringRequestParcel();
-        request.tetheringType = type;
-        request.localIPv4Address = serverAddr;
-        request.staticClientAddress = clientAddr;
-        request.exemptFromEntitlementCheck = exempt;
-        request.showProvisioningUi = false;
-        request.connectivityScope = scope;
-
-        return request;
+    private TetheringRequest createTetheringRequest(final int type,
+            final LinkAddress localIPv4Address, final LinkAddress staticClientAddress,
+            final boolean exempt, final int scope) {
+        TetheringRequest.Builder builder = new TetheringRequest.Builder(type)
+                .setExemptFromEntitlementCheck(exempt)
+                .setConnectivityScope(scope)
+                .setShouldShowEntitlementUi(false);
+        if (localIPv4Address != null && staticClientAddress != null) {
+            builder.setStaticIpv4Addresses(localIPv4Address, staticClientAddress);
+        }
+        return builder.build();
     }
 
     @NonNull
@@ -911,7 +910,7 @@
 
     private void prepareNcmTethering() {
         // Emulate startTethering(TETHERING_NCM) called
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_NCM), TEST_CALLER_PKG,
                 null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -919,7 +918,7 @@
 
     private void prepareUsbTethering() {
         // Emulate pressing the USB tethering button in Settings UI.
-        final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
+        final TetheringRequest request = createTetheringRequest(TETHERING_USB);
         mTethering.startTethering(request, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
 
@@ -1083,8 +1082,8 @@
         UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         assertSetIfaceToDadProxy(0 /* numOfCalls */, "" /* ifaceName */);
@@ -1112,8 +1111,8 @@
         UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         // TODO: add interfaceParams to compare in verify.
@@ -1128,8 +1127,8 @@
         UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
         verify(mRouterAdvertisementDaemon, times(1)).start();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -1146,13 +1145,12 @@
         UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME,
-                TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
@@ -1173,10 +1171,10 @@
         UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
 
         // Then 464xlat comes up
         upstreamState = buildMobile464xlatUpstreamState();
@@ -1188,12 +1186,11 @@
         mLooper.dispatchAll();
 
         // Forwarding is added for 464xlat
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME,
-                TEST_XLAT_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
         // Forwarding was not re-added for v6 (still times(1))
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
         // DHCP not restarted on downstream (still times(1))
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -1909,7 +1906,7 @@
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
                 null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
@@ -1938,7 +1935,7 @@
         when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
                 null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
@@ -1988,7 +1985,7 @@
         doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
 
         // Emulate pressing the WiFi tethering button.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
                 null);
         mLooper.dispatchAll();
         verify(mWifiManager, times(1)).startTetheredHotspot(null);
@@ -2334,7 +2331,7 @@
         tetherState = callback.pollTetherStatesChanged();
         assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
 
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
                 null);
         sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         tetherState = callback.pollTetherStatesChanged();
@@ -2430,11 +2427,11 @@
         initTetheringOnTestThread();
         final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_ETHERNET), TEST_CALLER_PKG,
                 null);
         mLooper.dispatchAll();
         verify(mEm, times(1)).requestTetheredInterface(any(), any());
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_ETHERNET), TEST_CALLER_PKG,
                 null);
         mLooper.dispatchAll();
         verifyNoMoreInteractions(mEm);
@@ -2644,7 +2641,7 @@
         final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR);
 
         // Enable USB tethering and check that Tethering starts USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_USB), TEST_CALLER_PKG,
                 firstResult);
         mLooper.dispatchAll();
         firstResult.assertHasResult();
@@ -2652,7 +2649,7 @@
         verifyNoMoreInteractions(mUsbManager);
 
         // Enable USB tethering again with the same request and expect no change to USB.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_USB), TEST_CALLER_PKG,
                 secondResult);
         mLooper.dispatchAll();
         secondResult.assertHasResult();
@@ -2661,7 +2658,7 @@
 
         // Enable USB tethering with a different request and expect that USB is stopped and
         // started.
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
+        mTethering.startTethering(createTetheringRequest(TETHERING_USB,
                   serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
                   TEST_CALLER_PKG, thirdResult);
         mLooper.dispatchAll();
@@ -2692,7 +2689,7 @@
         final int clientAddrParceled = 0xc0a8002a;
         final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
                 ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
+        mTethering.startTethering(createTetheringRequest(TETHERING_USB,
                   serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
                   TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
@@ -2820,8 +2817,8 @@
     public void testExemptFromEntitlementCheck() throws Exception {
         initTetheringOnTestThread();
         setupForRequiredProvisioning();
-        final TetheringRequestParcel wifiNotExemptRequest =
-                createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
+        final TetheringRequest wifiNotExemptRequest =
+                createTetheringRequest(TETHERING_WIFI, null, null, false,
                         CONNECTIVITY_SCOPE_GLOBAL);
         mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
@@ -2834,8 +2831,8 @@
         reset(mEntitleMgr);
 
         setupForRequiredProvisioning();
-        final TetheringRequestParcel wifiExemptRequest =
-                createTetheringRequestParcel(TETHERING_WIFI, null, null, true,
+        final TetheringRequest wifiExemptRequest =
+                createTetheringRequest(TETHERING_WIFI, null, null, true,
                         CONNECTIVITY_SCOPE_GLOBAL);
         mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
@@ -2954,7 +2951,7 @@
         when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
         final ArgumentCaptor<TetheredInterfaceCallback> callbackCaptor =
                 ArgumentCaptor.forClass(TetheredInterfaceCallback.class);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET),
+        mTethering.startTethering(createTetheringRequest(TETHERING_ETHERNET),
                 TEST_CALLER_PKG, null);
         mLooper.dispatchAll();
         verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture());
@@ -3235,7 +3232,7 @@
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+        mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
                 TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
@@ -3272,7 +3269,7 @@
 
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+        mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
                 TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
@@ -3294,7 +3291,7 @@
         // already bound.
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
         final ResultListener secondResult = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+        mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
                 TEST_CALLER_PKG, secondResult);
         mLooper.dispatchAll();
         verifySetBluetoothTethering(true /* enable */, false /* bindToPanService */);
@@ -3317,7 +3314,7 @@
         initTetheringOnTestThread();
         final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+        mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
                 TEST_CALLER_PKG, result);
         mLooper.dispatchAll();
         ServiceListener panListener = verifySetBluetoothTethering(true /* enable */,
@@ -3436,8 +3433,7 @@
         runUsbTethering(upstreamState);
 
         verify(mNetd).interfaceGetList();
-        verify(mNetd).tetherAddForward(expectedIface, TEST_MOBILE_IFNAME);
-        verify(mNetd).ipfwdAddInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager).addInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
 
         verify(mRouterAdvertisementDaemon).start();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
@@ -3487,7 +3483,7 @@
         // If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
         // available.
         final ResultListener ncmResult = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
-        mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+        mTethering.startTethering(createTetheringRequest(TETHERING_NCM), TEST_CALLER_PKG,
                 ncmResult);
         mLooper.dispatchAll();
         ncmResult.assertHasResult();
@@ -3622,6 +3618,42 @@
                 InetAddresses.parseNumericAddress(ifaceConfig.ipv4Addr), ifaceConfig.prefixLength);
         assertFalse(sapPrefix.equals(lohsPrefix));
     }
+
+    @Test
+    public void testWifiTetheringWhenP2pActive() throws Exception {
+        initTetheringOnTestThread();
+        // Enable wifi P2P.
+        sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+        verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
+        verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
+        verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+        // Verify never enable upstream if only P2P active.
+        verify(mUpstreamNetworkMonitor, never()).setTryCell(true);
+        assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
+
+        when(mWifiManager.startTetheredHotspot(any())).thenReturn(true);
+        // Emulate pressing the WiFi tethering button.
+        mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
+                null);
+        mLooper.dispatchAll();
+        verify(mWifiManager).startTetheredHotspot(null);
+        verifyNoMoreInteractions(mWifiManager);
+
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+
+        verify(mWifiManager).updateInterfaceIpState(TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        verifyNoMoreInteractions(mWifiManager);
+        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_TETHER);
+
+        verify(mUpstreamNetworkMonitor).setTryCell(true);
+    }
+
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index e2c924c..7cef9cb 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -46,10 +46,11 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
 import android.net.NetworkCapabilities;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
@@ -60,10 +61,12 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.networkstack.tethering.UpstreamNetworkState;
+import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 @RunWith(AndroidJUnit4.class)
@@ -76,28 +79,23 @@
     private static final long TEST_START_TIME = 1670395936033L;
     private static final long SECOND_IN_MILLIS = 1_000L;
 
+    @Mock private Context mContext;
+    @Mock private Dependencies mDeps;
+
     private TetheringMetrics mTetheringMetrics;
     private final NetworkTetheringReported.Builder mStatsBuilder =
             NetworkTetheringReported.newBuilder();
 
     private long mElapsedRealtime;
 
-    private class MockTetheringMetrics extends TetheringMetrics {
-        @Override
-        public void write(final NetworkTetheringReported reported) {}
-        @Override
-        public long timeNow() {
-            return currentTimeMillis();
-        }
-    }
-
     private long currentTimeMillis() {
         return TEST_START_TIME + mElapsedRealtime;
     }
 
     private void incrementCurrentTime(final long duration) {
         mElapsedRealtime += duration;
-        mTetheringMetrics.timeNow();
+        final long currentTimeMillis = currentTimeMillis();
+        doReturn(currentTimeMillis).when(mDeps).timeNow();
     }
 
     private long getElapsedRealtime() {
@@ -106,12 +104,14 @@
 
     private void clearElapsedRealtime() {
         mElapsedRealtime = 0;
+        doReturn(TEST_START_TIME).when(mDeps).timeNow();
     }
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mTetheringMetrics = spy(new MockTetheringMetrics());
+        doReturn(TEST_START_TIME).when(mDeps).timeNow();
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mElapsedRealtime = 0L;
     }
 
@@ -126,7 +126,7 @@
                 .setUpstreamEvents(upstreamEvents)
                 .setDurationMillis(duration)
                 .build();
-        verify(mTetheringMetrics).write(expectedReport);
+        verify(mDeps).write(expectedReport);
     }
 
     private void updateErrorAndSendReport(final int downstream, final int error) {
@@ -162,6 +162,7 @@
 
     private void runDownstreamTypesTest(final int type, final DownstreamType expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
         final long duration = 2 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
@@ -172,9 +173,7 @@
 
         verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN,
                 upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -189,6 +188,7 @@
 
     private void runErrorCodesTest(final int errorCode, final ErrorCode expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
         mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
         final long duration = 2 * SECOND_IN_MILLIS;
@@ -199,9 +199,7 @@
         addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN,
                     upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -231,6 +229,7 @@
 
     private void runUserTypesTest(final String callerPkg, final UserType expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
         final long duration = 1 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
@@ -241,9 +240,7 @@
         addUpstreamEvent(upstreamEvents, UpstreamType.UT_NO_NETWORK, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult,
                     upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -256,6 +253,7 @@
 
     private void runUpstreamTypesTest(final UpstreamNetworkState ns,
             final UpstreamType expectedResult) throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
         mTetheringMetrics.maybeUpdateUpstreamType(ns);
         final long duration = 2 * SECOND_IN_MILLIS;
@@ -266,9 +264,7 @@
         addUpstreamEvent(upstreamEvents, expectedResult, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
                 UserType.USER_UNKNOWN, upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -379,4 +375,21 @@
                 UserType.USER_SETTINGS, upstreamEvents,
                 currentTimeMillis() - wifiTetheringStartTime);
     }
+
+    private void runUsageSupportedForUpstreamTypeTest(final UpstreamType upstreamType,
+            final boolean isSupported) {
+        final boolean result = TetheringMetrics.isUsageSupportedForUpstreamType(upstreamType);
+        assertEquals(isSupported, result);
+    }
+
+    @Test
+    public void testUsageSupportedForUpstreamTypeTest() {
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_CELLULAR, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_BLUETOOTH, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_ETHERNET, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI_AWARE, false /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
index f8e98e3..2417385 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
@@ -19,9 +19,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.util.State
+import com.android.net.module.util.SyncStateMachine
+import com.android.net.module.util.SyncStateMachine.StateInfo
 import com.android.networkstack.tethering.util.StateMachineShim.AsyncStateMachine
 import com.android.networkstack.tethering.util.StateMachineShim.Dependencies
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
 import kotlin.test.assertFailsWith
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringUtilsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringUtilsTest.java
index 94ce2b6..f0770f9 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringUtilsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/TetheringUtilsTest.java
@@ -15,8 +15,6 @@
  */
 package com.android.networkstack.tethering.util;
 
-import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
-import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.system.OsConstants.AF_UNIX;
 import static android.system.OsConstants.EAGAIN;
@@ -25,8 +23,6 @@
 import static android.system.OsConstants.SOCK_NONBLOCK;
 
 import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
 
 import android.net.LinkAddress;
 import android.net.MacAddress;
@@ -43,9 +39,7 @@
 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.testutils.MiscAsserts;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -61,13 +55,6 @@
     private static final LinkAddress TEST_CLIENT_ADDR = new LinkAddress("192.168.43.5/24");
     private static final int PACKET_SIZE = 1500;
 
-    private TetheringRequestParcel mTetheringRequest;
-
-    @Before
-    public void setUp() {
-        mTetheringRequest = makeTetheringRequestParcel();
-    }
-
     public TetheringRequestParcel makeTetheringRequestParcel() {
         final TetheringRequestParcel request = new TetheringRequestParcel();
         request.tetheringType = TETHERING_WIFI;
@@ -78,40 +65,6 @@
         return request;
     }
 
-    @Test
-    public void testIsTetheringRequestEquals() {
-        TetheringRequestParcel request = makeTetheringRequestParcel();
-
-        assertTrue(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, mTetheringRequest));
-        assertTrue(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-        assertTrue(TetheringUtils.isTetheringRequestEquals(null, null));
-        assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, null));
-        assertFalse(TetheringUtils.isTetheringRequestEquals(null, mTetheringRequest));
-
-        request = makeTetheringRequestParcel();
-        request.tetheringType = TETHERING_USB;
-        assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
-        request = makeTetheringRequestParcel();
-        request.localIPv4Address = null;
-        request.staticClientAddress = null;
-        assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
-        request = makeTetheringRequestParcel();
-        request.exemptFromEntitlementCheck = true;
-        assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
-        request = makeTetheringRequestParcel();
-        request.showProvisioningUi = false;
-        assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
-        request = makeTetheringRequestParcel();
-        request.connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
-        assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request));
-
-        MiscAsserts.assertFieldCountEquals(6, TetheringRequestParcel.class);
-    }
-
     // Writes the specified packet to a filedescriptor, skipping the Ethernet header.
     // Needed because the Ipv6Utils methods for building packets always include the Ethernet header,
     // but socket filters applied by TetheringUtils expect the packet to start from the IP header.
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
index b7dc66e..ed4f3da 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/VersionedBroadcastListenerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.networkstack.tethering.util;
 
+import static com.android.testutils.HandlerUtils.waitForIdle;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.reset;
 
@@ -23,7 +25,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Handler;
-import android.os.Looper;
+import android.os.HandlerThread;
 import android.os.UserHandle;
 
 import androidx.test.filters.SmallTest;
@@ -33,7 +35,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -44,9 +45,11 @@
 public class VersionedBroadcastListenerTest {
     private static final String TAG = VersionedBroadcastListenerTest.class.getSimpleName();
     private static final String ACTION_TEST = "action.test.happy.broadcasts";
+    private static final long TEST_TIMEOUT_MS = 10_000L;
 
     @Mock private Context mContext;
     private BroadcastInterceptingContext mServiceContext;
+    private HandlerThread mHandlerThread;
     private Handler mHandler;
     private VersionedBroadcastListener mListener;
     private int mCallbackCount;
@@ -61,18 +64,13 @@
         }
     }
 
-    @BeforeClass
-    public static void setUpBeforeClass() throws Exception {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-    }
-
     @Before public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         reset(mContext);
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
         mServiceContext = new MockContext(mContext);
-        mHandler = new Handler(Looper.myLooper());
         mCallbackCount = 0;
         final IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_TEST);
@@ -85,11 +83,15 @@
             mListener.stopListening();
             mListener = null;
         }
+        mHandlerThread.quitSafely();
+        mHandlerThread.join(TEST_TIMEOUT_MS);
     }
 
     private void sendBroadcast() {
         final Intent intent = new Intent(ACTION_TEST);
         mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        // Sending the broadcast is synchronous, but the receiver just posts on the handler
+        waitForIdle(mHandler, TEST_TIMEOUT_MS);
     }
 
     @Test
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 674cd98..f6717c5 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -38,6 +38,7 @@
     cflags: [
         "-Wall",
         "-Werror",
+        "-Wextra",
     ],
     sdk_version: "30",
     min_sdk_version: "30",
@@ -65,73 +66,46 @@
 bpf {
     name: "block.o",
     srcs: ["block.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
 bpf {
     name: "dscpPolicy.o",
     srcs: ["dscpPolicy.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
 bpf {
     name: "offload.o",
     srcs: ["offload.c"],
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
+    btf: false,
 }
 
+// This version ships to Android T+ which uses mainline netbpfload.
 bpf {
-    name: "offload@btf.o",
-    srcs: ["offload@btf.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-DBTF",
-    ],
+    name: "offload@mainline.o",
+    srcs: ["offload@mainline.c"],
+    cflags: ["-DMAINLINE"],
 }
 
+// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
 bpf {
     name: "test.o",
     srcs: ["test.c"],
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
+    btf: false,
 }
 
+// This version ships to Android T+ which uses mainline netbpfload.
 bpf {
-    name: "test@btf.o",
-    srcs: ["test@btf.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-DBTF",
-    ],
+    name: "test@mainline.o",
+    srcs: ["test@mainline.c"],
+    cflags: ["-DMAINLINE"],
 }
 
 bpf {
     name: "clatd.o",
     srcs: ["clatd.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     sub_dir: "net_shared",
 }
 
@@ -139,11 +113,6 @@
     // WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
     name: "netd.o",
     srcs: ["netd.c"],
-    btf: true,
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
     // WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
     sub_dir: "netd_shared",
 }
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index 0a2b0b8..152dda6 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -19,8 +19,8 @@
 #include <netinet/in.h>
 #include <stdint.h>
 
-// The resulting .o needs to load on the Android T bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
 #include "bpf_helpers.h"
 
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index f3c7de5..ba2f26b 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -35,6 +35,7 @@
 
 // this returns 0 iff skb->sk is NULL
 static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
+static uint64_t (*bpf_get_sk_cookie)(struct bpf_sock* sk) = (void*)BPF_FUNC_get_socket_cookie;
 
 static uint32_t (*bpf_get_socket_uid)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_uid;
 
@@ -82,7 +83,7 @@
 
 // try to make the first 'len' header bytes readable/writable via direct packet access
 // (note: AFAIK there is no way to ask for only direct packet read without also getting write)
-static inline __always_inline void try_make_writable(struct __sk_buff* skb, int len) {
+static inline __always_inline void try_make_writable(struct __sk_buff* skb, unsigned len) {
     if (len > skb->len) len = skb->len;
     if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
 }
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index addb02f..da6ccbf 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -30,8 +30,8 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
-// The resulting .o needs to load on the Android T bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
@@ -177,7 +177,7 @@
 
     // Calculate the IPv4 one's complement checksum of the IPv4 header.
     __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
         sum4 += ((__u16*)&ip)[i];
     }
     // Note that sum4 is guaranteed to be non-zero by virtue of ip.version == 4
@@ -188,7 +188,7 @@
     // Calculate the *negative* IPv6 16-bit one's complement checksum of the IPv6 header.
     __wsum sum6 = 0;
     // We'll end up with a non-zero sum due to ip6->version == 6 (which has '0' bits)
-    for (int i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
         sum6 += ~((__u16*)ip6)[i];  // note the bitwise negation
     }
 
@@ -265,6 +265,10 @@
         *(struct iphdr*)data = ip;
     }
 
+    // Count successfully translated packet
+    __sync_fetch_and_add(&v->packets, 1);
+    __sync_fetch_and_add(&v->bytes, skb->len - l2_header_size);
+
     // Redirect, possibly back to same interface, so tcpdump sees packet twice.
     if (v->oif) return bpf_redirect(v->oif, BPF_F_INGRESS);
 
@@ -317,7 +321,7 @@
 
     // Calculate the IPv4 one's complement checksum of the IPv4 header.
     __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
         sum4 += ((__u16*)ip4)[i];
     }
     // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
@@ -383,7 +387,7 @@
     // Calculate the IPv6 16-bit one's complement checksum of the IPv6 header.
     __wsum sum6 = 0;
     // We'll end up with a non-zero sum due to ip6.version == 6
-    for (int i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
         sum6 += ((__u16*)&ip6)[i];
     }
 
@@ -416,6 +420,10 @@
     // Copy over the new ipv6 header without an ethernet header.
     *(struct ipv6hdr*)data = ip6;
 
+    // Count successfully translated packet
+    __sync_fetch_and_add(&v->packets, 1);
+    __sync_fetch_and_add(&v->bytes, skb->len);
+
     // Redirect to non v4-* interface.  Tcpdump only sees packet after this redirect.
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
diff --git a/bpf_progs/clatd.h b/bpf_progs/clatd.h
index b5f1cdc..a75798f 100644
--- a/bpf_progs/clatd.h
+++ b/bpf_progs/clatd.h
@@ -39,8 +39,10 @@
 typedef struct {
     uint32_t oif;           // The output interface to redirect to (0 means don't redirect)
     struct in_addr local4;  // The destination IPv4 address
+    uint64_t packets;       // Count of translated gso (large) packets
+    uint64_t bytes;         // Sum of post-translation skb->len
 } ClatIngress6Value;
-STRUCT_SIZE(ClatIngress6Value, 4 + 4);  // 8
+STRUCT_SIZE(ClatIngress6Value, 4 + 4 + 8 + 8);  // 24
 
 typedef struct {
     uint32_t iif;           // The input interface index
@@ -54,7 +56,9 @@
     struct in6_addr pfx96;   // The destination /96 nat64 prefix, bottom 32 bits must be 0
     bool oifIsEthernet;      // Whether the output interface requires ethernet header
     uint8_t pad[3];
+    uint64_t packets;       // Count of translated gso (large) packets
+    uint64_t bytes;         // Sum of post-translation skb->len
 } ClatEgress4Value;
-STRUCT_SIZE(ClatEgress4Value, 4 + 2 * 16 + 1 + 3);  // 40
+STRUCT_SIZE(ClatEgress4Value, 4 + 2 * 16 + 1 + 3 + 8 + 8);  // 56
 
 #undef STRUCT_SIZE
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
index e845a69..ed114e4 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf_progs/dscpPolicy.c
@@ -27,8 +27,8 @@
 #include <stdint.h>
 #include <string.h>
 
-// The resulting .o needs to load on the Android T bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
 #include "bpf_helpers.h"
 #include "dscpPolicy.h"
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 5e401aa..f08b007 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-// The resulting .o needs to load on the Android T bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
 #include <bpf_helpers.h>
 #include <linux/bpf.h>
@@ -97,19 +97,22 @@
 DEFINE_BPF_MAP_NO_NETD(ingress_discard_map, HASH, IngressDiscardKey, IngressDiscardValue,
                        INGRESS_DISCARD_MAP_SIZE)
 
+DEFINE_BPF_MAP_RW_NETD(lock_array_test_map, ARRAY, uint32_t, bool, 1)
+DEFINE_BPF_MAP_RW_NETD(lock_hash_test_map, HASH, uint32_t, bool, 1)
+
 /* never actually used from ebpf */
 DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
 
 // A single-element configuration array, packet tracing is enabled when 'true'.
 DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
                    AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
-                   BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
+                   BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
                    LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // A ring buffer on which packet information is pushed.
 DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
                        AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
-                       BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
+                       BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
                        LOAD_ON_USER, LOAD_ON_USERDEBUG);
 
 DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
@@ -139,6 +142,11 @@
 #define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
     DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE)
 
+#define DEFINE_NETD_V_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV)            \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV,                        \
+                        KVER_INF, BPFLOADER_MAINLINE_V_VERSION, BPFLOADER_MAX_VER, MANDATORY,     \
+                        "fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
 // programs that only need to be usable by the system server
 #define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
     DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF,  \
@@ -176,7 +184,7 @@
     static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
                                                               const TypeOfKey* const key,        \
                                                               const struct egress_bool egress,   \
-                                                              const struct kver_uint kver) {     \
+                                                     __unused const struct kver_uint kver) {     \
         StatsValue* value = bpf_##the_stats_map##_lookup_elem(key);                              \
         if (!value) {                                                                            \
             StatsValue newValue = {};                                                            \
@@ -407,6 +415,9 @@
 
     BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
 
+    // BACKGROUND match does not apply to loopback traffic
+    if (skb->ifindex == 1) enabledRules &= ~BACKGROUND_MATCH;
+
     UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid);
     uint32_t uidRules = uidEntry ? uidEntry->rule : 0;
     uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
@@ -513,22 +524,12 @@
     return match;
 }
 
-// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
-DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
-                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
-                    "fs_bpf_netd_readonly", "",
-                    IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
-(struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
-}
-
-// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+// Tracing on Android U+ 5.8+
 DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
                     bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
-                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
                     "fs_bpf_netd_readonly", "",
-                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+                    LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
 }
@@ -545,22 +546,12 @@
     return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
 }
 
-// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
-DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
-                    bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
-                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
-                    "fs_bpf_netd_readonly", "",
-                    IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
-(struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
-}
-
-// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
+// Tracing on Android U+ 5.8+
 DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
                     bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
-                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+                    BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
                     "fs_bpf_netd_readonly", "",
-                    LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+                    LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
 }
@@ -644,7 +635,8 @@
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
-    if (denylistMatch) return denylistMatch->rule & PENALTY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
+    uint32_t penalty_box = PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH;
+    if (denylistMatch) return denylistMatch->rule & penalty_box ? BPF_MATCH : BPF_NOMATCH;
     return BPF_NOMATCH;
 }
 
@@ -662,13 +654,86 @@
     return permissions ? *permissions : BPF_PERMISSION_INTERNET;
 }
 
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
                           KVER_4_14)
-(struct bpf_sock* sk) {
+(__unused struct bpf_sock* sk) {
     // A return value of 1 means allow, everything else means deny.
     return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
 }
 
+DEFINE_NETD_V_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
+                            inet_socket_release, KVER_5_10)
+(struct bpf_sock* sk) {
+    uint64_t cookie = bpf_get_sk_cookie(sk);
+    if (cookie) bpf_cookie_tag_map_delete_elem(&cookie);
+
+    return 1;
+}
+
+static __always_inline inline int check_localhost(__unused struct bpf_sock_addr *ctx) {
+    // See include/uapi/linux/bpf.h:
+    //
+    // struct bpf_sock_addr {
+    //   __u32 user_family;	//     R: 4 byte
+    //   __u32 user_ip4;	// BE, R: 1,2,4-byte,   W: 4-byte
+    //   __u32 user_ip6[4];	// BE, R: 1,2,4,8-byte, W: 4,8-byte
+    //   __u32 user_port;	// BE, R: 1,2,4-byte,   W: 4-byte
+    //   __u32 family;		//     R: 4 byte
+    //   __u32 type;		//     R: 4 byte
+    //   __u32 protocol;	//     R: 4 byte
+    //   __u32 msg_src_ip4;	// BE, R: 1,2,4-byte,   W: 4-byte
+    //   __u32 msg_src_ip6[4];	// BE, R: 1,2,4,8-byte, W: 4,8-byte
+    //   __bpf_md_ptr(struct bpf_sock *, sk);
+    // };
+    return 1;
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("connect6/inet6_connect", AID_ROOT, AID_ROOT, inet6_connect, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg4/udp4_recvmsg", AID_ROOT, AID_ROOT, udp4_recvmsg, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("recvmsg6/udp6_recvmsg", AID_ROOT, AID_ROOT, udp6_recvmsg, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg4/udp4_sendmsg", AID_ROOT, AID_ROOT, udp4_sendmsg, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("sendmsg6/udp6_sendmsg", AID_ROOT, AID_ROOT, udp6_sendmsg, KVER_4_14)
+(struct bpf_sock_addr *ctx) {
+    return check_localhost(ctx);
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("getsockopt/prog", AID_ROOT, AID_ROOT, getsockopt_prog, KVER_5_4)
+(struct bpf_sockopt *ctx) {
+    // Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
+    // This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
+    ctx->optlen = 0;
+    return 1; // ALLOW
+}
+
+DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
+(struct bpf_sockopt *ctx) {
+    // Tell kernel to use/process original buffer provided by userspace.
+    // This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
+    ctx->optlen = 0;
+    return 1; // ALLOW
+}
+
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity and netd");
 DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 098147f..4877a4b 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -155,7 +155,16 @@
 ASSERT_STRING_EQUAL(XT_BPF_ALLOWLIST_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf");
 ASSERT_STRING_EQUAL(XT_BPF_DENYLIST_PROG_PATH,  BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf");
 
-#define CGROUP_SOCKET_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
+#define CGROUP_INET_CREATE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
+#define CGROUP_INET_RELEASE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsockrelease_inet_release"
+#define CGROUP_CONNECT4_PROG_PATH BPF_NETD_PATH "prog_netd_connect4_inet4_connect"
+#define CGROUP_CONNECT6_PROG_PATH BPF_NETD_PATH "prog_netd_connect6_inet6_connect"
+#define CGROUP_UDP4_RECVMSG_PROG_PATH BPF_NETD_PATH "prog_netd_recvmsg4_udp4_recvmsg"
+#define CGROUP_UDP6_RECVMSG_PROG_PATH BPF_NETD_PATH "prog_netd_recvmsg6_udp6_recvmsg"
+#define CGROUP_UDP4_SENDMSG_PROG_PATH BPF_NETD_PATH "prog_netd_sendmsg4_udp4_sendmsg"
+#define CGROUP_UDP6_SENDMSG_PROG_PATH BPF_NETD_PATH "prog_netd_sendmsg6_udp6_sendmsg"
+#define CGROUP_GETSOCKOPT_PROG_PATH BPF_NETD_PATH "prog_netd_getsockopt_prog"
+#define CGROUP_SETSOCKOPT_PROG_PATH BPF_NETD_PATH "prog_netd_setsockopt_prog"
 
 #define TC_BPF_INGRESS_ACCOUNT_PROG_NAME "prog_netd_schedact_ingress_account"
 #define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_NETD_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
@@ -181,7 +190,7 @@
 enum UidOwnerMatchType : uint32_t {
     NO_MATCH = 0,
     HAPPY_BOX_MATCH = (1 << 0),
-    PENALTY_BOX_MATCH = (1 << 1),
+    PENALTY_BOX_USER_MATCH = (1 << 1),
     DOZABLE_MATCH = (1 << 2),
     STANDBY_MATCH = (1 << 3),
     POWERSAVE_MATCH = (1 << 4),
@@ -192,7 +201,8 @@
     OEM_DENY_1_MATCH = (1 << 9),
     OEM_DENY_2_MATCH = (1 << 10),
     OEM_DENY_3_MATCH = (1 << 11),
-    BACKGROUND_MATCH = (1 << 12)
+    BACKGROUND_MATCH = (1 << 12),
+    PENALTY_BOX_ADMIN_MATCH = (1 << 13),
 };
 // LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java)
 
@@ -260,5 +270,5 @@
 static inline bool is_system_uid(uint32_t uid) {
     // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
     // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
-    return (uid < AID_APP_START);
+    return ((uid % AID_USER_OFFSET) < AID_APP_START);
 }
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 90f96a1..4d908d2 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -24,16 +24,16 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
-#ifdef BTF
+#ifdef MAINLINE
 // BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
 // ship a different file than for later versions, but we need bpfloader v0.25+
 // for obj@ver.o support
-#define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
-#else /* BTF */
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#else /* MAINLINE */
 // The resulting .o needs to load on the Android S bpfloader
 #define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
-#define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
-#endif /* BTF */
+#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
+#endif /* MAINLINE */
 
 // Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
 #define TETHERING_UID AID_ROOT
@@ -127,7 +127,7 @@
 static inline __always_inline int do_forward6(struct __sk_buff* skb,
                                               const struct rawip_bool rawip,
                                               const struct stream_bool stream,
-                                              const struct kver_uint kver) {
+                                              __unused const struct kver_uint kver) {
     const bool is_ethernet = !rawip.rawip;
 
     // Must be meta-ethernet IPv6 frame
@@ -343,13 +343,13 @@
 // and define no-op stubs for pre-4.14 kernels.
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
@@ -363,7 +363,7 @@
         const int l2_header_size, void* data, const void* data_end,
         struct ethhdr* eth, struct iphdr* ip, const struct rawip_bool rawip,
         const struct stream_bool stream, const struct updatetime_bool updatetime,
-        const bool is_tcp, const struct kver_uint kver) {
+        const bool is_tcp, __unused const struct kver_uint kver) {
     const bool is_ethernet = !rawip.rawip;
     struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
     struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
@@ -593,7 +593,7 @@
 
     // Calculate the IPv4 one's complement checksum of the IPv4 header.
     __wsum sum4 = 0;
-    for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+    for (unsigned i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
         sum4 += ((__u16*)ip)[i];
     }
     // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
@@ -780,13 +780,13 @@
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
@@ -794,13 +794,13 @@
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
 DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
                            sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
-(struct __sk_buff* skb) {
+(__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
@@ -808,13 +808,13 @@
 
 DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
 
-static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
-        const struct stream_bool stream) {
+static inline __always_inline int do_xdp_forward6(__unused struct xdp_md *ctx,
+        __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
     return XDP_PASS;
 }
 
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
-        const struct stream_bool stream) {
+static inline __always_inline int do_xdp_forward4(__unused struct xdp_md *ctx,
+        __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
     return XDP_PASS;
 }
 
diff --git a/bpf_progs/offload@btf.c b/bpf_progs/offload@mainline.c
similarity index 100%
rename from bpf_progs/offload@btf.c
rename to bpf_progs/offload@mainline.c
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index 70b08b7..6a4471c 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -18,16 +18,16 @@
 #include <linux/in.h>
 #include <linux/ip.h>
 
-#ifdef BTF
+#ifdef MAINLINE
 // BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
 // ship a different file than for later versions, but we need bpfloader v0.25+
 // for obj@ver.o support
-#define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
-#else /* BTF */
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#else /* MAINLINE */
 // The resulting .o needs to load on the Android S bpfloader
 #define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
-#define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
-#endif /* BTF */
+#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
+#endif /* MAINLINE */
 
 // Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
 #define TETHERING_UID AID_ROOT
@@ -45,6 +45,10 @@
 // Used only by TetheringPrivilegedTests, not by production code.
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
                    TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether2_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
+                   TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether3_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
+                   TETHERING_GID)
 // Used only by BpfBitmapTest, not by production code.
 DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
 
diff --git a/bpf_progs/test@btf.c b/bpf_progs/test@mainline.c
similarity index 100%
rename from bpf_progs/test@btf.c
rename to bpf_progs/test@mainline.c
diff --git a/common/Android.bp b/common/Android.bp
index 0048a0a..5fabf41 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -26,7 +26,7 @@
 // as the above target may not exist
 // depending on the branch
 
-// The library requires the final artifact to contain net-utils-device-common-struct.
+// The library requires the final artifact to contain net-utils-device-common-struct-base.
 java_library {
     name: "connectivity-net-module-utils-bpf",
     srcs: [
@@ -45,7 +45,7 @@
         // For libraries which are statically linked in framework-connectivity, do not
         // statically link here because callers of this library might already have a static
         // version linked.
-        "net-utils-device-common-struct",
+        "net-utils-device-common-struct-base",
     ],
     apex_available: [
         "com.android.tethering",
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index 56625c5..fef9ac3 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -17,15 +17,25 @@
 aconfig_declarations {
     name: "com.android.net.flags-aconfig",
     package: "com.android.net.flags",
-    container: "system",
+    container: "com.android.tethering",
     srcs: ["flags.aconfig"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
+java_aconfig_library {
+    name: "com.android.net.flags-aconfig-java",
+    aconfig_declarations: "com.android.net.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
 aconfig_declarations {
     name: "com.android.net.thread.flags-aconfig",
     package: "com.android.net.thread.flags",
-    container: "system",
+    container: "com.android.tethering",
     srcs: ["thread_flags.aconfig"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
@@ -33,7 +43,24 @@
 aconfig_declarations {
     name: "nearby_flags",
     package: "com.android.nearby.flags",
-    container: "system",
+    container: "com.android.tethering",
     srcs: ["nearby_flags.aconfig"],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
+
+aconfig_declarations {
+    name: "com.android.networksecurity.flags-aconfig",
+    package: "com.android.net.ct.flags",
+    container: "com.android.tethering",
+    srcs: ["networksecurity_flags.aconfig"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_aconfig_library {
+    name: "networksecurity_flags_java_lib",
+    aconfig_declarations: "com.android.networksecurity.flags-aconfig",
+    min_sdk_version: "30",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    apex_available: ["com.android.tethering"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 19b522c..1b0da4e 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -1,11 +1,12 @@
 package: "com.android.net.flags"
-container: "system"
+container: "com.android.tethering"
 
 # This file contains aconfig flags for FlaggedAPI annotations
 # Flags used from platform code must be in under frameworks
 
 flag {
   name: "set_data_saver_via_cm"
+  is_exported: true
   namespace: "android_core_networking"
   description: "Set data saver through ConnectivityManager API"
   bug: "297836825"
@@ -13,6 +14,7 @@
 
 flag {
   name: "support_is_uid_networking_blocked"
+  is_exported: true
   namespace: "android_core_networking"
   description: "This flag controls whether isUidNetworkingBlocked is supported"
   bug: "297836825"
@@ -20,6 +22,7 @@
 
 flag {
   name: "basic_background_restrictions_enabled"
+  is_exported: true
   namespace: "android_core_networking"
   description: "Block network access for apps in a low importance background state"
   bug: "304347838"
@@ -27,6 +30,7 @@
 
 flag {
   name: "ipsec_transform_state"
+  is_exported: true
   namespace: "android_core_networking_ipsec"
   description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
   bug: "308011229"
@@ -34,6 +38,7 @@
 
 flag {
   name: "tethering_request_with_soft_ap_config"
+  is_exported: true
   namespace: "android_core_networking"
   description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
   bug: "216524590"
@@ -41,6 +46,7 @@
 
 flag {
   name: "request_restricted_wifi"
+  is_exported: true
   namespace: "android_core_networking"
   description: "Flag for API to support requesting restricted wifi"
   bug: "315835605"
@@ -48,6 +54,7 @@
 
 flag {
   name: "net_capability_local_network"
+  is_exported: true
   namespace: "android_core_networking"
   description: "Flag for local network capability API"
   bug: "313000440"
@@ -55,6 +62,7 @@
 
 flag {
   name: "support_transport_satellite"
+  is_exported: true
   namespace: "android_core_networking"
   description: "Flag for satellite transport API"
   bug: "320514105"
@@ -62,6 +70,7 @@
 
 flag {
   name: "nsd_subtypes_support_enabled"
+  is_exported: true
   namespace: "android_core_networking"
   description: "Flag for API to support nsd subtypes"
   bug: "265095929"
@@ -69,7 +78,56 @@
 
 flag {
   name: "register_nsd_offload_engine_api"
+  is_exported: true
   namespace: "android_core_networking"
   description: "Flag for API to register nsd offload engine"
   bug: "301713539"
 }
+
+flag {
+  name: "metered_network_firewall_chains"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for metered network firewall chain API"
+  bug: "332628891"
+}
+
+flag {
+  name: "blocked_reason_oem_deny_chains"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for oem deny chains blocked reasons API"
+  bug: "328732146"
+}
+
+flag {
+  name: "blocked_reason_network_restricted"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
+  bug: "339559837"
+}
+
+flag {
+  name: "net_capability_not_bandwidth_constrained"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED API"
+  bug: "343823469"
+}
+
+flag {
+  name: "tethering_request_virtual"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for introducing TETHERING_VIRTUAL type"
+  bug: "340376953"
+}
+
+flag {
+  name: "netstats_add_entries"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for NetworkStats#addEntries API"
+  bug: "335680025"
+}
diff --git a/common/nearby_flags.aconfig b/common/nearby_flags.aconfig
index b957d33..55a865b 100644
--- a/common/nearby_flags.aconfig
+++ b/common/nearby_flags.aconfig
@@ -1,8 +1,9 @@
 package: "com.android.nearby.flags"
-container: "system"
+container: "com.android.tethering"
 
 flag {
     name: "powered_off_finding"
+    is_exported: true
     namespace: "nearby"
     description: "Controls whether the Powered Off Finding feature is enabled"
     bug: "307898240"
diff --git a/common/networksecurity_flags.aconfig b/common/networksecurity_flags.aconfig
new file mode 100644
index 0000000..ef8ffcd
--- /dev/null
+++ b/common/networksecurity_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.net.ct.flags"
+container: "com.android.tethering"
+flag {
+    name: "certificate_transparency_service"
+    is_exported: true
+    namespace: "network_security"
+    description: "Enable service for certificate transparency log list data"
+    bug: "319829948"
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
index 69fab09..71f7516 100644
--- a/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
+++ b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
@@ -36,11 +36,24 @@
     @Field(order = 3, type = Type.U8, padding = 3)
     public final short oifIsEthernet; // Whether the output interface requires ethernet header
 
+    @Field(order = 4, type = Type.U63)
+    public final long packets; // Count of translated gso (large) packets
+
+    @Field(order = 5, type = Type.U63)
+    public final long bytes; // Sum of post-translation skb->len
+
     public ClatEgress4Value(final int oif, final Inet6Address local6, final Inet6Address pfx96,
-            final short oifIsEthernet) {
+            final short oifIsEthernet, final long packets, final long bytes) {
         this.oif = oif;
         this.local6 = local6;
         this.pfx96 = pfx96;
         this.oifIsEthernet = oifIsEthernet;
+        this.packets = packets;
+        this.bytes = bytes;
+    }
+
+    public ClatEgress4Value(final int oif, final Inet6Address local6, final Inet6Address pfx96,
+            final short oifIsEthernet) {
+        this(oif, local6, pfx96, oifIsEthernet, 0, 0);
     }
 }
diff --git a/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
index fb81caa..25f737b 100644
--- a/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
+++ b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
@@ -30,8 +30,21 @@
     @Field(order = 1, type = Type.Ipv4Address)
     public final Inet4Address local4; // The destination IPv4 address
 
-    public ClatIngress6Value(final int oif, final Inet4Address local4) {
+    @Field(order = 2, type = Type.U63)
+    public final long packets; // Count of translated gso (large) packets
+
+    @Field(order = 3, type = Type.U63)
+    public final long bytes; // Sum of post-translation skb->len
+
+    public ClatIngress6Value(final int oif, final Inet4Address local4, final long packets,
+            final long bytes) {
         this.oif = oif;
         this.local4 = local4;
+        this.packets = packets;
+        this.bytes = bytes;
+    }
+
+    public ClatIngress6Value(final int oif, final Inet4Address local4) {
+        this(oif, local4, 0, 0);
     }
 }
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 09595a6..43acd1b 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -1,8 +1,9 @@
 package: "com.android.net.thread.flags"
-container: "system"
+container: "com.android.tethering"
 
 flag {
     name: "thread_enabled"
+    is_exported: true
     namespace: "thread_network"
     description: "Controls whether the Android Thread feature is enabled"
     bug: "301473012"
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index bc919ac..ac78d09 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -137,6 +137,10 @@
     // framework-connectivity-pre-jarjar match at runtime.
     jarjar_rules: ":framework-connectivity-jarjar-rules",
     stub_only_libs: [
+        // static_libs is not used to compile stubs. So libs which have
+        // been included in static_libs might still need to
+        // be in stub_only_libs to be usable when generating the API stubs.
+        "com.android.net.flags-aconfig-java",
         // Use prebuilt framework-connectivity stubs to avoid circular dependencies
         "sdk_module-lib_current_framework-connectivity",
     ],
@@ -201,6 +205,9 @@
         "com.android.net.thread.flags-aconfig",
         "nearby_flags",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // This rule is not used anymore(b/268440216).
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 1f1953c..2354882 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -310,6 +310,7 @@
   public final class NetworkStats implements java.lang.Iterable<android.net.NetworkStats.Entry> android.os.Parcelable {
     ctor public NetworkStats(long, int);
     method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
+    method @FlaggedApi("com.android.net.flags.netstats_add_entries") @NonNull public android.net.NetworkStats addEntries(@NonNull java.util.List<android.net.NetworkStats.Entry>);
     method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
     method public android.net.NetworkStats clone();
     method public int describeContents();
@@ -497,16 +498,26 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
   }
 
+  @FlaggedApi("com.android.net.thread.flags.configuration_enabled") public final class ThreadConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public boolean isDhcpv6PdEnabled();
+    method public boolean isNat64Enabled();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ThreadConfiguration> CREATOR;
+  }
+
   @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
     method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
     method public int getThreadVersion();
     method public static boolean isAttached(int);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void leave(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void registerConfigurationCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void unregisterConfigurationCallback(@NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
     field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
diff --git a/framework-t/lint-baseline.xml b/framework-t/lint-baseline.xml
new file mode 100644
index 0000000..4e206ed
--- /dev/null
+++ b/framework-t/lint-baseline.xml
@@ -0,0 +1,1313 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="Field `SERVICE_NAME` is a flagged API and should be inside an `if (ThreadNetworkFlags.threadEnabled())` check (or annotate the surrounding method `registerServiceWrappers` with `@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) to transfer requirement to caller`)"
+        errorLine1="                ThreadNetworkManager.SERVICE_NAME,"
+        errorLine2="                                     ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java"
+            line="101"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Class `ThreadNetworkManager` is a flagged API and should be inside an `if (ThreadNetworkFlags.threadEnabled())` check (or annotate the surrounding method `registerServiceWrappers` with `@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) to transfer requirement to caller`)"
+        errorLine1="                ThreadNetworkManager.class,"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java"
+            line="102"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `ThreadNetworkManager()` is a flagged API and should be inside an `if (ThreadNetworkFlags.threadEnabled())` check (or annotate the surrounding method `registerServiceWrappers` with `@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) to transfer requirement to caller`)"
+        errorLine1="                    return new ThreadNetworkManager(context, managerService);"
+        errorLine2="                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java"
+            line="106"
+            column="28"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="87"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="87"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="87"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="95"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="95"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="95"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="95"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="529"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="573"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="605"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="This is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `handleMessage` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="                    final String s = getNsdServiceInfoType((DiscoveryRequest) obj);"
+        errorLine2="                                                            ~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+            line="1076"
+            column="61"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getServiceType()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `getNsdServiceInfoType` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="        return r.getServiceType();"
+        errorLine2="               ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+            line="1236"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `Builder()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="        DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)"
+        errorLine2="                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+            line="1477"
+            column="36"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `build()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="        DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)"
+        errorLine2="                                   ^">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+            line="1477"
+            column="36"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `setNetwork()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="        DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)"
+        errorLine2="                                   ^">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+            line="1477"
+            column="36"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `discoverServices()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="        discoverServices(request, executor, listener);"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+            line="1479"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `Builder()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="                new DiscoveryRequest.Builder(protocolType, serviceType).build();"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+            line="1566"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `build()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `discoverServices` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="                new DiscoveryRequest.Builder(protocolType, serviceType).build();"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdManager.java"
+            line="1566"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getSubtypes()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `NsdServiceInfo` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="        mSubtypes = new ArraySet&lt;>(other.getSubtypes());"
+        errorLine2="                                   ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdServiceInfo.java"
+            line="106"
+            column="36"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `setSubtypes()` is a flagged API and should be inside an `if (Flags.nsdSubtypesSupportEnabled())` check (or annotate the surrounding method `createFromParcel` with `@FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED) to transfer requirement to caller`)"
+        errorLine1="                info.setSubtypes(new ArraySet&lt;>(in.createStringArrayList()));"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/NsdServiceInfo.java"
+            line="673"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+</issues>
diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
index 7fa0661..18c839f 100644
--- a/framework-t/src/android/app/usage/NetworkStatsManager.java
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -752,8 +752,8 @@
     /**
      * Query realtime mobile network usage statistics.
      *
-     * Return a snapshot of current UID network statistics, as it applies
-     * to the mobile radios of the device. The snapshot will include any
+     * Return a snapshot of current UID network statistics for both cellular and satellite (which
+     * also uses same mobile radio as cellular) when called. The snapshot will include any
      * tethering traffic, video calling data usage and count of
      * network operations set by {@link TrafficStats#incrementOperationCount}
      * made over a mobile radio.
diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java
index b8070f0..719f60d 100644
--- a/framework-t/src/android/net/EthernetManager.java
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -642,7 +642,14 @@
     }
 
     /**
-     * Listen to changes in the state of ethernet.
+     * Register a IntConsumer to be called back on ethernet state changes.
+     *
+     * <p>{@link IntConsumer#accept} with the current ethernet state will be triggered immediately
+     * upon adding a listener. The same callback is invoked on Ethernet state change, i.e. when
+     * calling {@link #setEthernetEnabled}.
+     * <p>The reported state is represented by:
+     * {@link #ETHERNET_STATE_DISABLED}: ethernet is now disabled.
+     * {@link #ETHERNET_STATE_ENABLED}: ethernet is now enabled.
      *
      * @param executor to run callbacks on.
      * @param listener to listen ethernet state changed.
diff --git a/framework-t/src/android/net/INetworkStatsService.aidl b/framework-t/src/android/net/INetworkStatsService.aidl
index c86f7fd..7f0c1fe 100644
--- a/framework-t/src/android/net/INetworkStatsService.aidl
+++ b/framework-t/src/android/net/INetworkStatsService.aidl
@@ -101,4 +101,7 @@
      * Note that invocation of any interface will be sent to all providers.
      */
      void setStatsProviderWarningAndLimitAsync(String iface, long warning, long limit);
+
+     /** Clear TrafficStats rate-limit caches. */
+     void clearTrafficStatsRateLimitCaches();
 }
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 4e10a96..70c9bc8 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -124,7 +124,7 @@
     private IpSecTransform activate()
             throws IOException, IpSecManager.ResourceUnavailableException,
                     IpSecManager.SpiUnavailableException {
-        synchronized (this) {
+        synchronized (mLock) {
             try {
                 IpSecTransformResponse result = getIpSecManager(mContext).createTransform(
                         mConfig, new Binder(), mContext.getOpPackageName());
@@ -164,20 +164,23 @@
     public void close() {
         Log.d(TAG, "Removing Transform with Id " + mResourceId);
 
-        // Always safe to attempt cleanup
-        if (mResourceId == INVALID_RESOURCE_ID) {
-            mCloseGuard.close();
-            return;
-        }
-        try {
-            getIpSecManager(mContext).deleteTransform(mResourceId);
-        } catch (Exception e) {
-            // On close we swallow all random exceptions since failure to close is not
-            // actionable by the user.
-            Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
-        } finally {
-            mResourceId = INVALID_RESOURCE_ID;
-            mCloseGuard.close();
+        synchronized(mLock) {
+            // Always safe to attempt cleanup
+            if (mResourceId == INVALID_RESOURCE_ID) {
+                mCloseGuard.close();
+                return;
+            }
+
+            try {
+                    getIpSecManager(mContext).deleteTransform(mResourceId);
+            } catch (Exception e) {
+                // On close we swallow all random exceptions since failure to close is not
+                // actionable by the user.
+                Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+            } finally {
+                mResourceId = INVALID_RESOURCE_ID;
+                mCloseGuard.close();
+            }
         }
     }
 
@@ -196,14 +199,17 @@
     }
 
     private final IpSecConfig mConfig;
-    private int mResourceId;
+    private final Object mLock = new Object();
+    private int mResourceId; // Partly guarded by mLock to ensure basic safety, not correctness
     private final Context mContext;
     private final CloseGuard mCloseGuard = CloseGuard.get();
 
     /** @hide */
     @VisibleForTesting
     public int getResourceId() {
-        return mResourceId;
+        synchronized(mLock) {
+            return mResourceId;
+        }
     }
 
     /**
@@ -224,8 +230,10 @@
         // TODO: Consider adding check to prevent DDoS attack.
 
         try {
-            final IpSecTransformState ipSecTransformState =
-                    getIpSecManager(mContext).getTransformState(mResourceId);
+            IpSecTransformState ipSecTransformState;
+            synchronized(mLock) {
+                ipSecTransformState = getIpSecManager(mContext).getTransformState(mResourceId);
+            }
             executor.execute(
                     () -> {
                         callback.onResult(ipSecTransformState);
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index e9a3f58..a2c4fc3 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -18,6 +18,7 @@
 
 import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -33,6 +34,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.CollectionUtils;
 
 import libcore.util.EmptyArray;
@@ -845,6 +847,21 @@
     }
 
     /**
+     * Adds multiple entries to a copy of this NetworkStats instance.
+     *
+     * @param entries The entries to add.
+     * @return A new NetworkStats instance with the added entries.
+     */
+    @FlaggedApi(Flags.FLAG_NETSTATS_ADD_ENTRIES)
+    public @NonNull NetworkStats addEntries(@NonNull final List<Entry> entries) {
+        final NetworkStats newStats = this.clone();
+        for (final Entry entry : Objects.requireNonNull(entries)) {
+            newStats.combineValues(entry);
+        }
+        return newStats;
+    }
+
+    /**
      * Add given values with an existing row, or create a new row if
      * {@link #findIndex(String, int, int, int, int, int, int)} is unable to find match. Can
      * also be used to subtract values from existing rows.
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index a69b38d..77c8001 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -19,6 +19,7 @@
 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
@@ -692,6 +693,27 @@
         return UNSUPPORTED;
     }
 
+    /** Clear TrafficStats rate-limit caches.
+     *
+     * This is mainly for {@link com.android.server.net.NetworkStatsService} to
+     * clear rate-limit cache to avoid caching for TrafficStats API results.
+     * Tests might get stale values after generating network traffic, which
+     * generally need to wait for cache expiry to get updated values.
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    public static void clearRateLimitCaches() {
+        try {
+            getStatsService().clearTrafficStatsRateLimitCaches();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /**
      * Return the number of packets transmitted on the specified interface since the interface
      * was created. Statistics are measured at the network layer, so both TCP and
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index 2895b0c..6afb2d5 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -110,8 +110,9 @@
     }
 
     /**
-     * Returns the time interval that the resource records may be cached on a DNS resolver or
-     * {@code null} if not specified.
+     * Returns the time interval that the resource records may be cached on a DNS resolver.
+     *
+     * The value will be {@code null} if it's not specified with the {@link #Builder}.
      *
      * @hide
      */
@@ -161,7 +162,7 @@
         dest.writeParcelable(mServiceInfo, flags);
         dest.writeInt(mProtocolType);
         dest.writeLong(mAdvertisingConfig);
-        dest.writeLong(mTtl == null ? -1 : mTtl.getSeconds());
+        dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds());
     }
 
 //    @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
@@ -205,7 +206,9 @@
          * When registering a service, {@link NsdManager#FAILURE_BAD_PARAMETERS} will be returned
          * if {@code ttl} is smaller than 30 seconds.
          *
-         * Note: only number of seconds of {@code ttl} is used.
+         * Note: the value after the decimal point (in unit of seconds) will be discarded. For
+         * example, {@code 30} seconds will be used when {@code Duration.ofSeconds(30L, 50_000L)}
+         * is provided.
          *
          * @param ttl the maximum duration that the DNS resource records will be cached
          *
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 1001423..b21e22a 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -389,6 +389,7 @@
     }
 
     private static final int FIRST_LISTENER_KEY = 1;
+    private static final int DNSSEC_PROTOCOL = 3;
 
     private final INsdServiceConnector mService;
     private final Context mContext;
@@ -1754,45 +1755,132 @@
         }
     }
 
+    private enum ServiceValidationType {
+        NO_SERVICE,
+        HAS_SERVICE, // A service with a positive port
+        HAS_SERVICE_ZERO_PORT, // A service with a zero port
+    }
+
+    private enum HostValidationType {
+        DEFAULT_HOST, // No host is specified so the default host will be used
+        CUSTOM_HOST, // A custom host with addresses is specified
+        CUSTOM_HOST_NO_ADDRESS, // A custom host without address is specified
+    }
+
+    private enum PublicKeyValidationType {
+        NO_KEY,
+        HAS_KEY,
+    }
+
+    /**
+     * Check if the service is valid for registration and classify it as one of {@link
+     * ServiceValidationType}.
+     */
+    private static ServiceValidationType validateService(NsdServiceInfo serviceInfo) {
+        final boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName());
+        final boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType());
+        if (!hasServiceName && !hasServiceType && serviceInfo.getPort() == 0) {
+            return ServiceValidationType.NO_SERVICE;
+        }
+        if (!hasServiceName || !hasServiceType) {
+            throw new IllegalArgumentException("The service name or the service type is missing");
+        }
+        if (serviceInfo.getPort() < 0) {
+            throw new IllegalArgumentException("Invalid port");
+        }
+        if (serviceInfo.getPort() == 0) {
+            return ServiceValidationType.HAS_SERVICE_ZERO_PORT;
+        }
+        return ServiceValidationType.HAS_SERVICE;
+    }
+
+    /**
+     * Check if the host is valid for registration and classify it as one of {@link
+     * HostValidationType}.
+     */
+    private static HostValidationType validateHost(NsdServiceInfo serviceInfo) {
+        final boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname());
+        final boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses());
+        if (!hasHostname) {
+            // Keep compatible with the legacy behavior: It's allowed to set host
+            // addresses for a service registration although the host addresses
+            // won't be registered. To register the addresses for a host, the
+            // hostname must be specified.
+            return HostValidationType.DEFAULT_HOST;
+        }
+        if (!hasHostAddresses) {
+            return HostValidationType.CUSTOM_HOST_NO_ADDRESS;
+        }
+        return HostValidationType.CUSTOM_HOST;
+    }
+
+    /**
+     * Check if the public key is valid for registration and classify it as one of {@link
+     * PublicKeyValidationType}.
+     *
+     * <p>For simplicity, it only checks if the protocol is DNSSEC and the RDATA is not fewer than 4
+     * bytes. See RFC 3445 Section 3.
+     */
+    private static PublicKeyValidationType validatePublicKey(NsdServiceInfo serviceInfo) {
+        byte[] publicKey = serviceInfo.getPublicKey();
+        if (publicKey == null) {
+            return PublicKeyValidationType.NO_KEY;
+        }
+        if (publicKey.length < 4) {
+            throw new IllegalArgumentException("The public key should be at least 4 bytes long");
+        }
+        int protocol = publicKey[2];
+        if (protocol == DNSSEC_PROTOCOL) {
+            return PublicKeyValidationType.HAS_KEY;
+        }
+        throw new IllegalArgumentException(
+                "The public key's protocol ("
+                        + protocol
+                        + ") is invalid. It should be DNSSEC_PROTOCOL (3)");
+    }
+
     /**
      * Check if the {@link NsdServiceInfo} is valid for registration.
      *
-     * The following can be registered:
-     * - A service with an optional host.
-     * - A hostname with addresses.
+     * <p>Firstly, check if service, host and public key are all valid respectively. Then check if
+     * the combination of service, host and public key is valid.
      *
-     * Note that:
-     * - When registering a service, the service name, service type and port must be specified. If
-     *   hostname is specified, the host addresses can optionally be specified.
-     * - When registering a host without a service, the addresses must be specified.
+     * <p>If the {@code serviceInfo} is invalid, throw an {@link IllegalArgumentException}
+     * describing the reason.
+     *
+     * <p>There are the invalid combinations of service, host and public key:
+     *
+     * <ul>
+     *   <li>Neither service nor host is specified.
+     *   <li>No public key is specified and the service has a zero port.
+     *   <li>The registration only contains the hostname but addresses are missing.
+     * </ul>
+     *
+     * <p>Keys are used to reserve hostnames or service names while the service/host is temporarily
+     * inactive, so registrations with a key and just a hostname or a service name are acceptable.
      *
      * @hide
      */
     public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) {
         Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
-        boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName());
-        boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType());
-        boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname());
-        boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses());
 
-        if (serviceInfo.getPort() < 0) {
-            throw new IllegalArgumentException("Invalid port");
+        final ServiceValidationType serviceValidation = validateService(serviceInfo);
+        final HostValidationType hostValidation = validateHost(serviceInfo);
+        final PublicKeyValidationType publicKeyValidation = validatePublicKey(serviceInfo);
+
+        if (serviceValidation == ServiceValidationType.NO_SERVICE
+                && hostValidation == HostValidationType.DEFAULT_HOST) {
+            throw new IllegalArgumentException("Nothing to register");
         }
-
-        if (hasServiceType || hasServiceName || (serviceInfo.getPort() > 0)) {
-            if (!(hasServiceType && hasServiceName && (serviceInfo.getPort() > 0))) {
-                throw new IllegalArgumentException(
-                        "The service type, service name or port is missing");
+        if (publicKeyValidation == PublicKeyValidationType.NO_KEY) {
+            if (serviceValidation == ServiceValidationType.HAS_SERVICE_ZERO_PORT) {
+                throw new IllegalArgumentException("The port is missing");
             }
-        }
-
-        if (!hasServiceType && !hasHostname) {
-            throw new IllegalArgumentException("No service or host specified in NsdServiceInfo");
-        }
-
-        if (!hasServiceType && hasHostname && !hasHostAddresses) {
-            // TODO: b/317946010 - This may be allowed when it supports registering KEY RR.
-            throw new IllegalArgumentException("No host addresses specified in NsdServiceInfo");
+            if (serviceValidation == ServiceValidationType.NO_SERVICE
+                    && hostValidation == HostValidationType.CUSTOM_HOST_NO_ADDRESS) {
+                throw new IllegalArgumentException(
+                        "The host addresses must be specified unless there is a service");
+            }
         }
     }
 }
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index f4cc2ac..d8cccb2 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,8 @@
 
 package android.net.nsd;
 
+import static com.android.net.module.util.HexDump.toHexString;
+
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -35,10 +37,12 @@
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.StringJoiner;
 
 /**
  * A class representing service information for network service discovery
@@ -66,11 +70,15 @@
     private int mPort;
 
     @Nullable
+    private byte[] mPublicKey;
+
+    @Nullable
     private Network mNetwork;
 
     private int mInterfaceIndex;
 
-    // The timestamp that all resource records associated with this service are considered invalid.
+    // The timestamp that one or more resource records associated with this service are considered
+    // invalid.
     @Nullable
     private Instant mExpirationTime;
 
@@ -172,8 +180,18 @@
         return new ArrayList<>(mHostAddresses);
     }
 
-    /** Set the host addresses */
+    /**
+     * Set the host addresses.
+     *
+     * <p>When registering hosts/services, there can only be one registration including address
+     * records for a given hostname.
+     *
+     * <p>For example, if a client registers a service with the hostname "MyHost" and the address
+     * records of 192.168.1.1 and 192.168.1.2, then other registrations for the hostname "MyHost"
+     * must not have any address record, otherwise there will be a conflict.
+     */
     public void setHostAddresses(@NonNull List<InetAddress> addresses) {
+        // TODO: b/284905335 - Notify the client when there is a conflict.
         mHostAddresses.clear();
         mHostAddresses.addAll(addresses);
     }
@@ -216,6 +234,40 @@
     }
 
     /**
+     * Set the public key RDATA to be advertised in a KEY RR (RFC 2535).
+     *
+     * <p>This is the public key of the key pair used for signing a DNS message (e.g. SRP). Clients
+     * typically don't need this information, but the KEY RR is usually published to claim the use
+     * of the DNS name so that another mDNS advertiser can't take over the ownership during a
+     * temporary power down of the original host device.
+     *
+     * <p>When the public key is set to non-null, exactly one KEY RR will be advertised for each of
+     * the service and host name if they are not null.
+     *
+     * @hide // For Thread only
+     */
+    public void setPublicKey(@Nullable byte[] publicKey) {
+        if (publicKey == null) {
+            mPublicKey = null;
+            return;
+        }
+        mPublicKey = Arrays.copyOf(publicKey, publicKey.length);
+    }
+
+    /**
+     * Get the public key RDATA in the KEY RR (RFC 2535) or {@code null} if no KEY RR exists.
+     *
+     * @hide // For Thread only
+     */
+    @Nullable
+    public byte[] getPublicKey() {
+        if (mPublicKey == null) {
+            return null;
+        }
+        return Arrays.copyOf(mPublicKey, mPublicKey.length);
+    }
+
+    /**
      * Unpack txt information from a base-64 encoded byte array.
      *
      * @param txtRecordsRawBytes The raw base64 encoded byte array.
@@ -497,7 +549,9 @@
     /**
      * Sets the timestamp after when this service is expired.
      *
-     * Note: only number of seconds of {@code expirationTime} is used.
+     * Note: the value after the decimal point (in unit of seconds) will be discarded. For
+     * example, {@code 30} seconds will be used when {@code Duration.ofSeconds(30L, 50_000L)}
+     * is provided.
      *
      * @hide
      */
@@ -538,11 +592,50 @@
                 .append(", network: ").append(mNetwork)
                 .append(", expirationTime: ").append(mExpirationTime);
 
-        byte[] txtRecord = getTxtRecord();
-        sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
+        final StringJoiner txtJoiner =
+                new StringJoiner(", " /* delimiter */, "{" /* prefix */, "}" /* suffix */);
+
+        sb.append(", txtRecord: ");
+        for (int i = 0; i < mTxtRecord.size(); i++) {
+            txtJoiner.add(mTxtRecord.keyAt(i) + "=" + getPrintableTxtValue(mTxtRecord.valueAt(i)));
+        }
+        sb.append(txtJoiner.toString());
         return sb.toString();
     }
 
+    /**
+     * Returns printable string for {@code txtValue}.
+     *
+     * If {@code txtValue} contains non-printable ASCII characters, a HEX string with prefix "0x"
+     * will be returned. Otherwise, the ASCII string of {@code txtValue} is returned.
+     *
+     */
+    private static String getPrintableTxtValue(@Nullable byte[] txtValue) {
+        if (txtValue == null) {
+            return "(null)";
+        }
+
+        if (containsNonPrintableChars(txtValue)) {
+            return "0x" + toHexString(txtValue);
+        }
+
+        return new String(txtValue, StandardCharsets.US_ASCII);
+    }
+
+    /**
+     * Returns {@code true} if {@code txtValue} contains non-printable ASCII characters.
+     *
+     * The printable characters are in range of [32, 126].
+     */
+    private static boolean containsNonPrintableChars(byte[] txtValue) {
+        for (int i = 0; i < txtValue.length; i++) {
+            if (txtValue[i] < 32 || txtValue[i] > 126) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /** Implement the Parcelable interface */
     public int describeContents() {
         return 0;
@@ -577,6 +670,7 @@
         }
         dest.writeString(mHostname);
         dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
+        dest.writeByteArray(mPublicKey);
     }
 
     /** Implement the Parcelable interface */
@@ -609,6 +703,7 @@
                 info.mHostname = in.readString();
                 final long seconds = in.readLong();
                 info.setExpirationTime(seconds < 0 ? null : Instant.ofEpochSecond(seconds));
+                info.mPublicKey = in.createByteArray();
                 return info;
             }
 
diff --git a/framework/Android.bp b/framework/Android.bp
index 52f2c7c..4c4f792 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -63,6 +63,7 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
+        ":framework-networksecurity-sources",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -86,16 +87,18 @@
         "framework-wifi.stubs.module_lib",
     ],
     static_libs: [
+        "com.android.net.flags-aconfig-java",
         // Not using the latest stable version because all functions in the latest version of
         // mdns_aidl_interface are deprecated.
         "mdns_aidl_interface-V1-java",
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
+        "networksecurity_flags_java_lib",
         "framework-connectivity-javastream-protos",
     ],
     impl_only_static_libs: [
-        "net-utils-device-common-bpf",
+        "net-utils-framework-connectivity",
     ],
     libs: [
         "androidx.annotation_annotation",
@@ -108,22 +111,20 @@
     ],
 }
 
+// Library to allow Cronet to use hidden APIs
 java_library {
-    name: "framework-connectivity-pre-jarjar",
+    name: "framework-connectivity-pre-jarjar-without-cronet",
     defaults: [
         "framework-connectivity-defaults",
     ],
     static_libs: [
-        "httpclient_api",
-        "httpclient_impl",
-        "http_client_logging",
         // Framework-connectivity-pre-jarjar is identical to framework-connectivity
         // implementation, but without the jarjar rules. However, framework-connectivity
         // is not based on framework-connectivity-pre-jarjar, it's rebuilt from source
         // to generate the SDK stubs.
         // Even if the library is included in "impl_only_static_libs" of defaults. This is still
         // needed because java_library which doesn't understand "impl_only_static_libs".
-        "net-utils-device-common-bpf",
+        "net-utils-framework-connectivity",
     ],
     libs: [
         // This cannot be in the defaults clause above because if it were, it would be used
@@ -134,6 +135,21 @@
         "framework-tethering.impl",
         "framework-wifi.stubs.module_lib",
     ],
+    visibility: ["//external/cronet:__subpackages__"],
+}
+
+java_library {
+    name: "framework-connectivity-pre-jarjar",
+    defaults: ["framework-module-defaults"],
+    min_sdk_version: "30",
+    static_libs: [
+        "framework-connectivity-pre-jarjar-without-cronet",
+        "httpclient_api",
+        "httpclient_impl",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
@@ -145,7 +161,6 @@
     ],
     impl_only_static_libs: [
         "httpclient_impl",
-        "http_client_logging",
     ],
 }
 
@@ -194,6 +209,7 @@
     },
     aconfig_declarations: [
         "com.android.net.flags-aconfig",
+        "com.android.networksecurity.flags-aconfig",
     ],
 }
 
@@ -294,6 +310,7 @@
     srcs: [
         ":framework-connectivity-sources",
         ":framework-connectivity-tiramisu-updatable-sources",
+        ":framework-networksecurity-sources",
         ":framework-nearby-java-sources",
         ":framework-thread-sources",
     ],
@@ -332,7 +349,6 @@
     srcs: [
         // Files listed here MUST all be annotated with @RequiresApi(Build.VERSION_CODES.S)
         // or above as appropriate so that API checks are enforced for R+ users of this library
-        "src/android/net/RoutingCoordinatorManager.java",
         "src/android/net/connectivity/ConnectivityInternalApiUtil.java",
     ],
     visibility: [
diff --git a/framework/api/current.txt b/framework/api/current.txt
index ef8415c..7bc0cf3 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -337,6 +337,7 @@
     field public static final int NET_CAPABILITY_MCX = 23; // 0x17
     field public static final int NET_CAPABILITY_MMS = 0; // 0x0
     field public static final int NET_CAPABILITY_MMTEL = 33; // 0x21
+    field @FlaggedApi("com.android.net.flags.net_capability_not_bandwidth_constrained") public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37; // 0x25
     field public static final int NET_CAPABILITY_NOT_CONGESTED = 20; // 0x14
     field public static final int NET_CAPABILITY_NOT_METERED = 11; // 0xb
     field public static final int NET_CAPABILITY_NOT_RESTRICTED = 13; // 0xd
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 026d8a9..cd7307f 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -51,11 +51,16 @@
     field public static final int BLOCKED_REASON_DOZE = 2; // 0x2
     field public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16; // 0x10
     field public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 32; // 0x20
+    field @FlaggedApi("com.android.net.flags.blocked_reason_network_restricted") public static final int BLOCKED_REASON_NETWORK_RESTRICTED = 256; // 0x100
     field public static final int BLOCKED_REASON_NONE = 0; // 0x0
+    field @FlaggedApi("com.android.net.flags.blocked_reason_oem_deny_chains") public static final int BLOCKED_REASON_OEM_DENY = 128; // 0x80
     field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
     field @FlaggedApi("com.android.net.flags.basic_background_restrictions_enabled") public static final int FIREWALL_CHAIN_BACKGROUND = 6; // 0x6
     field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
     field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
+    field @FlaggedApi("com.android.net.flags.metered_network_firewall_chains") public static final int FIREWALL_CHAIN_METERED_ALLOW = 10; // 0xa
+    field @FlaggedApi("com.android.net.flags.metered_network_firewall_chains") public static final int FIREWALL_CHAIN_METERED_DENY_ADMIN = 12; // 0xc
+    field @FlaggedApi("com.android.net.flags.metered_network_firewall_chains") public static final int FIREWALL_CHAIN_METERED_DENY_USER = 11; // 0xb
     field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
     field public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8; // 0x8
     field public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9; // 0x9
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
index 2c0b15f..dddabef 100644
--- a/framework/lint-baseline.xml
+++ b/framework/lint-baseline.xml
@@ -375,4 +375,279 @@
             column="34"/>
     </issue>
 
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_BACKGROUND` is a flagged API and should be inside an `if (Flags.basicBackgroundRestrictionsEnabled())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED) to transfer requirement to caller`)"
+        errorLine1="            FIREWALL_CHAIN_BACKGROUND"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsConstants.java"
+            line="115"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_ALLOW` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            FIREWALL_CHAIN_METERED_ALLOW"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsConstants.java"
+            line="137"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_DENY_USER` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            FIREWALL_CHAIN_METERED_DENY_USER,"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsConstants.java"
+            line="146"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_DENY_ADMIN` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            FIREWALL_CHAIN_METERED_DENY_ADMIN"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsConstants.java"
+            line="147"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_BACKGROUND` is a flagged API and should be inside an `if (Flags.basicBackgroundRestrictionsEnabled())` check (or annotate the surrounding method `getMatchByFirewallChain` with `@FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED) to transfer requirement to caller`)"
+        errorLine1="            case FIREWALL_CHAIN_BACKGROUND:"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+            line="133"
+            column="18"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_ALLOW` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `getMatchByFirewallChain` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            case FIREWALL_CHAIN_METERED_ALLOW:"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+            line="143"
+            column="18"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_DENY_USER` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `getMatchByFirewallChain` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            case FIREWALL_CHAIN_METERED_DENY_USER:"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+            line="145"
+            column="18"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_DENY_ADMIN` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `getMatchByFirewallChain` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            case FIREWALL_CHAIN_METERED_DENY_ADMIN:"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+            line="147"
+            column="18"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `BLOCKED_REASON_APP_BACKGROUND` is a flagged API and should be inside an `if (Flags.basicBackgroundRestrictionsEnabled())` check (or annotate the surrounding method `getUidNetworkingBlockedReasons` with `@FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED) to transfer requirement to caller`)"
+        errorLine1="            blockedReasons |= BLOCKED_REASON_APP_BACKGROUND;"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+            line="293"
+            column="31"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `BLOCKED_REASON_OEM_DENY` is a flagged API and should be inside an `if (Flags.blockedReasonOemDenyChains())` check (or annotate the surrounding method `getUidNetworkingBlockedReasons` with `@FlaggedApi(Flags.BLOCKED_REASON_OEM_DENY_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            blockedReasons |= BLOCKED_REASON_OEM_DENY;"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/BpfNetMapsUtils.java"
+            line="296"
+            column="31"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_ALLOW` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `addUidToMeteredNetworkAllowList` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_ALLOW);"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+            line="6191"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_ALLOW` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `removeUidFromMeteredNetworkAllowList` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_DENY);"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+            line="6214"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_DENY_USER` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `addUidToMeteredNetworkDenyList` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_DENY);"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+            line="6243"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `FIREWALL_CHAIN_METERED_DENY_USER` is a flagged API and should be inside an `if (Flags.meteredNetworkFirewallChains())` check (or annotate the surrounding method `removeUidFromMeteredNetworkDenyList` with `@FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS) to transfer requirement to caller`)"
+        errorLine1="            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_ALLOW);"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+            line="6273"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+        errorLine1="    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;"
+        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+            line="767"
+            column="51"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+        errorLine1="            defaultCapabilities |= (1L &lt;&lt; NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);"
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+            line="818"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+        errorLine1="            (1L &lt;&lt; NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);"
+        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+            line="849"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getSubscriptionIds()` is a flagged API and should be inside an `if (Flags.requestRestrictedWifi())` check (or annotate the surrounding method `restrictCapabilitiesForTestNetwork` with `@FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI) to transfer requirement to caller`)"
+        errorLine1="        final Set&lt;Integer> originalSubIds = getSubscriptionIds();"
+        errorLine2="                                            ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+            line="1254"
+            column="45"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `TRANSPORT_SATELLITE` is a flagged API and should be inside an `if (Flags.supportTransportSatellite())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE) to transfer requirement to caller`)"
+        errorLine1="    public static final int MAX_TRANSPORT = TRANSPORT_SATELLITE;"
+        errorLine2="                                            ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+            line="1383"
+            column="45"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `TRANSPORT_SATELLITE` is a flagged API and should be inside an `if (Flags.supportTransportSatellite())` check (or annotate the surrounding method `specifierAcceptableForMultipleTransports` with `@FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE) to transfer requirement to caller`)"
+        errorLine1="                == (1 &lt;&lt; TRANSPORT_CELLULAR | 1 &lt;&lt; TRANSPORT_SATELLITE);"
+        errorLine2="                                                   ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+            line="1836"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `NET_CAPABILITY_LOCAL_NETWORK` is a flagged API and should be inside an `if (Flags.netCapabilityLocalNetwork())` check (or annotate the surrounding method `capabilityNameOf` with `@FlaggedApi(Flags.FLAG_NET_CAPABILITY_LOCAL_NETWORK) to transfer requirement to caller`)"
+        errorLine1="            case NET_CAPABILITY_LOCAL_NETWORK:        return &quot;LOCAL_NETWORK&quot;;"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+            line="2637"
+            column="18"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `capabilityNameOf` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+        errorLine1="            case NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED:    return &quot;NOT_BANDWIDTH_CONSTRAINED&quot;;"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkCapabilities.java"
+            line="2638"
+            column="18"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `TRANSPORT_SATELLITE` is a flagged API and should be inside an `if (Flags.supportTransportSatellite())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE) to transfer requirement to caller`)"
+        errorLine1="        TRANSPORT_SATELLITE"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java"
+            line="80"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED` is a flagged API and should be inside an `if (Flags.netCapabilityNotBandwidthConstrained())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED) to transfer requirement to caller`)"
+        errorLine1="                NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+            line="291"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getSubscriptionIds()` is a flagged API and should be inside an `if (Flags.requestRestrictedWifi())` check (or annotate the surrounding method `getSubscriptionIds` with `@FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI) to transfer requirement to caller`)"
+        errorLine1="        return networkCapabilities.getSubscriptionIds();"
+        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+            line="887"
+            column="16"/>
+    </issue>
+
 </issues>
\ No newline at end of file
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index 5d0fe73..f3773de 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -19,6 +19,9 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
@@ -67,7 +70,7 @@
     // LINT.IfChange(match_type)
     public static final long NO_MATCH = 0;
     public static final long HAPPY_BOX_MATCH = (1 << 0);
-    public static final long PENALTY_BOX_MATCH = (1 << 1);
+    public static final long PENALTY_BOX_USER_MATCH = (1 << 1);
     public static final long DOZABLE_MATCH = (1 << 2);
     public static final long STANDBY_MATCH = (1 << 3);
     public static final long POWERSAVE_MATCH = (1 << 4);
@@ -79,10 +82,11 @@
     public static final long OEM_DENY_2_MATCH = (1 << 10);
     public static final long OEM_DENY_3_MATCH = (1 << 11);
     public static final long BACKGROUND_MATCH = (1 << 12);
+    public static final long PENALTY_BOX_ADMIN_MATCH = (1 << 13);
 
     public static final List<Pair<Long, String>> MATCH_LIST = Arrays.asList(
             Pair.create(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"),
-            Pair.create(PENALTY_BOX_MATCH, "PENALTY_BOX_MATCH"),
+            Pair.create(PENALTY_BOX_USER_MATCH, "PENALTY_BOX_USER_MATCH"),
             Pair.create(DOZABLE_MATCH, "DOZABLE_MATCH"),
             Pair.create(STANDBY_MATCH, "STANDBY_MATCH"),
             Pair.create(POWERSAVE_MATCH, "POWERSAVE_MATCH"),
@@ -93,11 +97,13 @@
             Pair.create(OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH"),
             Pair.create(OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH"),
             Pair.create(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH"),
-            Pair.create(BACKGROUND_MATCH, "BACKGROUND_MATCH")
+            Pair.create(BACKGROUND_MATCH, "BACKGROUND_MATCH"),
+            Pair.create(PENALTY_BOX_ADMIN_MATCH, "PENALTY_BOX_ADMIN_MATCH")
     );
 
     /**
-     * List of all firewall allow chains.
+     * List of all firewall allow chains that are applied to all networks regardless of meteredness
+     * See {@link #METERED_ALLOW_CHAINS} for allow chains that are only applied to metered networks.
      *
      * Allow chains mean the firewall denies all uids by default, uids must be explicitly allowed.
      */
@@ -110,7 +116,8 @@
     );
 
     /**
-     * List of all firewall deny chains.
+     * List of all firewall deny chains that are applied to all networks regardless of meteredness
+     * See {@link #METERED_DENY_CHAINS} for deny chains that are only applied to metered networks.
      *
      * Deny chains mean the firewall allows all uids by default, uids must be explicitly denied.
      */
@@ -120,5 +127,24 @@
             FIREWALL_CHAIN_OEM_DENY_2,
             FIREWALL_CHAIN_OEM_DENY_3
     );
+
+    /**
+     * List of all firewall allow chains that are only applied to metered networks.
+     * See {@link #ALLOW_CHAINS} for allow chains that are applied to all networks regardless of
+     * meteredness.
+     */
+    public static final List<Integer> METERED_ALLOW_CHAINS = List.of(
+            FIREWALL_CHAIN_METERED_ALLOW
+    );
+
+    /**
+     * List of all firewall deny chains that are only applied to metered networks.
+     * See {@link #DENY_CHAINS} for deny chains that are applied to all networks regardless of
+     * meteredness.
+     */
+    public static final List<Integer> METERED_DENY_CHAINS = List.of(
+            FIREWALL_CHAIN_METERED_DENY_USER,
+            FIREWALL_CHAIN_METERED_DENY_ADMIN
+    );
     // LINT.ThenChange(../../../../bpf_progs/netd.h)
 }
diff --git a/framework/src/android/net/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
deleted file mode 100644
index ee422ab..0000000
--- a/framework/src/android/net/BpfNetMapsReader.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * 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 android.net;
-
-import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
-import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
-import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
-import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
-import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
-import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
-import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
-import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
-import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
-import static android.net.BpfNetMapsUtils.isFirewallAllowList;
-import static android.net.BpfNetMapsUtils.throwIfPreT;
-import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
-import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
-
-import android.annotation.NonNull;
-import android.annotation.RequiresApi;
-import android.os.Build;
-import android.os.ServiceSpecificException;
-import android.system.ErrnoException;
-import android.system.Os;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
-import com.android.net.module.util.BpfMap;
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.Struct.S32;
-import com.android.net.module.util.Struct.U32;
-import com.android.net.module.util.Struct.U8;
-
-/**
- * A helper class to *read* java BpfMaps.
- * @hide
- */
-@RequiresApi(Build.VERSION_CODES.TIRAMISU)  // BPF maps were only mainlined in T
-public class BpfNetMapsReader {
-    private static final String TAG = BpfNetMapsReader.class.getSimpleName();
-
-    // Locally store the handle of bpf maps. The FileDescriptors are statically cached inside the
-    // BpfMap implementation.
-
-    // Bpf map to store various networking configurations, the format of the value is different
-    // for different keys. See BpfNetMapsConstants#*_CONFIGURATION_KEY for keys.
-    private final IBpfMap<S32, U32> mConfigurationMap;
-    // Bpf map to store per uid traffic control configurations.
-    // See {@link UidOwnerValue} for more detail.
-    private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap;
-    private final IBpfMap<S32, U8> mDataSaverEnabledMap;
-    private final Dependencies mDeps;
-
-    // Bitmaps for calculating whether a given uid is blocked by firewall chains.
-    private static final long sMaskDropIfSet;
-    private static final long sMaskDropIfUnset;
-
-    static {
-        long maskDropIfSet = 0L;
-        long maskDropIfUnset = 0L;
-
-        for (int chain : BpfNetMapsConstants.ALLOW_CHAINS) {
-            final long match = getMatchByFirewallChain(chain);
-            maskDropIfUnset |= match;
-        }
-        for (int chain : BpfNetMapsConstants.DENY_CHAINS) {
-            final long match = getMatchByFirewallChain(chain);
-            maskDropIfSet |= match;
-        }
-        sMaskDropIfSet = maskDropIfSet;
-        sMaskDropIfUnset = maskDropIfUnset;
-    }
-
-    private static class SingletonHolder {
-        static final BpfNetMapsReader sInstance = new BpfNetMapsReader();
-    }
-
-    @NonNull
-    public static BpfNetMapsReader getInstance() {
-        return SingletonHolder.sInstance;
-    }
-
-    private BpfNetMapsReader() {
-        this(new Dependencies());
-    }
-
-    // While the production code uses the singleton to optimize for performance and deal with
-    // concurrent access, the test needs to use a non-static approach for dependency injection and
-    // mocking virtual bpf maps.
-    @VisibleForTesting
-    public BpfNetMapsReader(@NonNull Dependencies deps) {
-        if (!SdkLevel.isAtLeastT()) {
-            throw new UnsupportedOperationException(
-                    BpfNetMapsReader.class.getSimpleName() + " is not supported below Android T");
-        }
-        mDeps = deps;
-        mConfigurationMap = mDeps.getConfigurationMap();
-        mUidOwnerMap = mDeps.getUidOwnerMap();
-        mDataSaverEnabledMap = mDeps.getDataSaverEnabledMap();
-    }
-
-    /**
-     * Dependencies of BpfNetMapReader, for injection in tests.
-     */
-    @VisibleForTesting
-    public static class Dependencies {
-        /** Get the configuration map. */
-        public IBpfMap<S32, U32> getConfigurationMap() {
-            try {
-                return new BpfMap<>(CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDONLY,
-                        S32.class, U32.class);
-            } catch (ErrnoException e) {
-                throw new IllegalStateException("Cannot open configuration map", e);
-            }
-        }
-
-        /** Get the uid owner map. */
-        public IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
-            try {
-                return new BpfMap<>(UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDONLY,
-                        S32.class, UidOwnerValue.class);
-            } catch (ErrnoException e) {
-                throw new IllegalStateException("Cannot open uid owner map", e);
-            }
-        }
-
-        /** Get the data saver enabled map. */
-        public  IBpfMap<S32, U8> getDataSaverEnabledMap() {
-            try {
-                return new BpfMap<>(DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDONLY, S32.class,
-                        U8.class);
-            } catch (ErrnoException e) {
-                throw new IllegalStateException("Cannot open data saver enabled map", e);
-            }
-        }
-    }
-
-    /**
-     * Get the specified firewall chain's status.
-     *
-     * @param chain target chain
-     * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
-     * @throws UnsupportedOperationException if called on pre-T devices.
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    public boolean isChainEnabled(final int chain) {
-        return isChainEnabled(mConfigurationMap, chain);
-    }
-
-    /**
-     * Get firewall rule of specified firewall chain on specified uid.
-     *
-     * @param chain target chain
-     * @param uid        target uid
-     * @return either {@link ConnectivityManager#FIREWALL_RULE_ALLOW} or
-     *         {@link ConnectivityManager#FIREWALL_RULE_DENY}.
-     * @throws UnsupportedOperationException if called on pre-T devices.
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    public int getUidRule(final int chain, final int uid) {
-        return getUidRule(mUidOwnerMap, chain, uid);
-    }
-
-    /**
-     * Get the specified firewall chain's status.
-     *
-     * @param configurationMap target configurationMap
-     * @param chain target chain
-     * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
-     * @throws UnsupportedOperationException if called on pre-T devices.
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    public static boolean isChainEnabled(
-            final IBpfMap<S32, U32> configurationMap, final int chain) {
-        throwIfPreT("isChainEnabled is not available on pre-T devices");
-
-        final long match = getMatchByFirewallChain(chain);
-        try {
-            final U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
-            return (config.val & match) != 0;
-        } catch (ErrnoException e) {
-            throw new ServiceSpecificException(e.errno,
-                    "Unable to get firewall chain status: " + Os.strerror(e.errno));
-        }
-    }
-
-    /**
-     * Get firewall rule of specified firewall chain on specified uid.
-     *
-     * @param uidOwnerMap target uidOwnerMap.
-     * @param chain target chain.
-     * @param uid target uid.
-     * @return either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
-     * @throws UnsupportedOperationException if called on pre-T devices.
-     * @throws ServiceSpecificException      in case of failure, with an error code indicating the
-     *                                       cause of the failure.
-     */
-    public static int getUidRule(final IBpfMap<S32, UidOwnerValue> uidOwnerMap,
-            final int chain, final int uid) {
-        throwIfPreT("getUidRule is not available on pre-T devices");
-
-        final long match = getMatchByFirewallChain(chain);
-        final boolean isAllowList = isFirewallAllowList(chain);
-        try {
-            final UidOwnerValue uidMatch = uidOwnerMap.getValue(new S32(uid));
-            final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
-            return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
-        } catch (ErrnoException e) {
-            throw new ServiceSpecificException(e.errno,
-                    "Unable to get uid rule status: " + Os.strerror(e.errno));
-        }
-    }
-
-    /**
-     * Return whether the network is blocked by firewall chains for the given uid.
-     *
-     * @param uid The target uid.
-     * @param isNetworkMetered Whether the target network is metered.
-     * @param isDataSaverEnabled Whether the data saver is enabled.
-     *
-     * @return True if the network is blocked. Otherwise, false.
-     * @throws ServiceSpecificException if the read fails.
-     *
-     * @hide
-     */
-    public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered,
-            boolean isDataSaverEnabled) {
-        throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
-
-        final long uidRuleConfig;
-        final long uidMatch;
-        try {
-            uidRuleConfig = mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
-            final UidOwnerValue value = mUidOwnerMap.getValue(new S32(uid));
-            uidMatch = (value != null) ? value.rule : 0L;
-        } catch (ErrnoException e) {
-            throw new ServiceSpecificException(e.errno,
-                    "Unable to get firewall chain status: " + Os.strerror(e.errno));
-        }
-
-        final boolean blockedByAllowChains = 0 != (uidRuleConfig & ~uidMatch & sMaskDropIfUnset);
-        final boolean blockedByDenyChains = 0 != (uidRuleConfig & uidMatch & sMaskDropIfSet);
-        if (blockedByAllowChains || blockedByDenyChains) {
-            return true;
-        }
-
-        if (!isNetworkMetered) return false;
-        if ((uidMatch & PENALTY_BOX_MATCH) != 0) return true;
-        if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
-        return isDataSaverEnabled;
-    }
-
-    /**
-     * Get Data Saver enabled or disabled
-     *
-     * @return whether Data Saver is enabled or disabled.
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    public boolean getDataSaverEnabled() {
-        throwIfPreT("getDataSaverEnabled is not available on pre-T devices");
-
-        try {
-            return mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val == DATA_SAVER_ENABLED;
-        } catch (ErrnoException e) {
-            throw new ServiceSpecificException(e.errno, "Unable to get data saver: "
-                    + Os.strerror(e.errno));
-        }
-    }
-}
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 0be30bb..1760fa7 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -18,32 +18,69 @@
 
 import static android.net.BpfNetMapsConstants.ALLOW_CHAINS;
 import static android.net.BpfNetMapsConstants.BACKGROUND_MATCH;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
 import static android.net.BpfNetMapsConstants.DENY_CHAINS;
 import static android.net.BpfNetMapsConstants.DOZABLE_MATCH;
+import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.LOW_POWER_STANDBY_MATCH;
 import static android.net.BpfNetMapsConstants.MATCH_LIST;
+import static android.net.BpfNetMapsConstants.METERED_ALLOW_CHAINS;
+import static android.net.BpfNetMapsConstants.METERED_DENY_CHAINS;
 import static android.net.BpfNetMapsConstants.NO_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_1_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_2_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_3_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_ADMIN_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_USER_MATCH;
 import static android.net.BpfNetMapsConstants.POWERSAVE_MATCH;
 import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
 import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
+import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISABLED;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_OEM_DENY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_RESTRICTED_MODE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.system.OsConstants.EINVAL;
 
+import android.os.Build;
+import android.os.Process;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.util.Pair;
 
+import androidx.annotation.RequiresApi;
+
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.S32;
+import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
 
 import java.util.StringJoiner;
 
@@ -55,7 +92,29 @@
 // Note that this class should be put into bootclasspath instead of static libraries.
 // Because modules could have different copies of this class if this is statically linked,
 // which would be problematic if the definitions in these modules are not synchronized.
+// Note that NetworkStack can not use this before U due to b/326143935
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class BpfNetMapsUtils {
+    // Bitmaps for calculating whether a given uid is blocked by firewall chains.
+    private static final long sMaskDropIfSet;
+    private static final long sMaskDropIfUnset;
+
+    static {
+        long maskDropIfSet = 0L;
+        long maskDropIfUnset = 0L;
+
+        for (int chain : BpfNetMapsConstants.ALLOW_CHAINS) {
+            final long match = getMatchByFirewallChain(chain);
+            maskDropIfUnset |= match;
+        }
+        for (int chain : BpfNetMapsConstants.DENY_CHAINS) {
+            final long match = getMatchByFirewallChain(chain);
+            maskDropIfSet |= match;
+        }
+        sMaskDropIfSet = maskDropIfSet;
+        sMaskDropIfUnset = maskDropIfUnset;
+    }
+
     // Prevent this class from being accidental instantiated.
     private BpfNetMapsUtils() {}
 
@@ -82,6 +141,12 @@
                 return OEM_DENY_2_MATCH;
             case FIREWALL_CHAIN_OEM_DENY_3:
                 return OEM_DENY_3_MATCH;
+            case FIREWALL_CHAIN_METERED_ALLOW:
+                return HAPPY_BOX_MATCH;
+            case FIREWALL_CHAIN_METERED_DENY_USER:
+                return PENALTY_BOX_USER_MATCH;
+            case FIREWALL_CHAIN_METERED_DENY_ADMIN:
+                return PENALTY_BOX_ADMIN_MATCH;
             default:
                 throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
         }
@@ -94,9 +159,9 @@
      * DENYLIST means the firewall allows all by default, uids must be explicitly denied
      */
     public static boolean isFirewallAllowList(final int chain) {
-        if (ALLOW_CHAINS.contains(chain)) {
+        if (ALLOW_CHAINS.contains(chain) || METERED_ALLOW_CHAINS.contains(chain)) {
             return true;
-        } else if (DENY_CHAINS.contains(chain)) {
+        } else if (DENY_CHAINS.contains(chain) || METERED_DENY_CHAINS.contains(chain)) {
             return false;
         }
         throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
@@ -133,4 +198,178 @@
             throw new UnsupportedOperationException(msg);
         }
     }
+
+    /**
+     * Get the specified firewall chain's status.
+     *
+     * @param configurationMap target configurationMap
+     * @param chain target chain
+     * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public static boolean isChainEnabled(
+            final IBpfMap<S32, U32> configurationMap, final int chain) {
+        throwIfPreT("isChainEnabled is not available on pre-T devices");
+
+        final long match = getMatchByFirewallChain(chain);
+        try {
+            final U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+            return (config.val & match) != 0;
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Unable to get firewall chain status: " + Os.strerror(e.errno));
+        }
+    }
+
+    /**
+     * Get firewall rule of specified firewall chain on specified uid.
+     *
+     * @param uidOwnerMap target uidOwnerMap.
+     * @param chain target chain.
+     * @param uid target uid.
+     * @return either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException      in case of failure, with an error code indicating the
+     *                                       cause of the failure.
+     */
+    public static int getUidRule(final IBpfMap<S32, UidOwnerValue> uidOwnerMap,
+            final int chain, final int uid) {
+        throwIfPreT("getUidRule is not available on pre-T devices");
+
+        final long match = getMatchByFirewallChain(chain);
+        final boolean isAllowList = isFirewallAllowList(chain);
+        try {
+            final UidOwnerValue uidMatch = uidOwnerMap.getValue(new S32(uid));
+            final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
+            return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Unable to get uid rule status: " + Os.strerror(e.errno));
+        }
+    }
+
+    /**
+     * Get blocked reasons for specified uid
+     *
+     * @param uid Target Uid
+     * @return Reasons of network access blocking for an UID
+     */
+    public static int getUidNetworkingBlockedReasons(final int uid,
+            IBpfMap<S32, U32> configurationMap,
+            IBpfMap<S32, UidOwnerValue> uidOwnerMap,
+            IBpfMap<S32, U8> dataSaverEnabledMap
+    ) {
+        // System uids are not blocked by firewall chains, see bpf_progs/netd.c
+        // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
+        if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
+            return BLOCKED_REASON_NONE;
+        }
+
+        final long uidRuleConfig;
+        final long uidMatch;
+        try {
+            uidRuleConfig = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
+            final UidOwnerValue value = uidOwnerMap.getValue(new Struct.S32(uid));
+            uidMatch = (value != null) ? value.rule : 0L;
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Unable to get firewall chain status: " + Os.strerror(e.errno));
+        }
+        final long blockingMatches = (uidRuleConfig & ~uidMatch & sMaskDropIfUnset)
+                | (uidRuleConfig & uidMatch & sMaskDropIfSet);
+
+        int blockedReasons = BLOCKED_REASON_NONE;
+        if ((blockingMatches & POWERSAVE_MATCH) != 0) {
+            blockedReasons |= BLOCKED_REASON_BATTERY_SAVER;
+        }
+        if ((blockingMatches & DOZABLE_MATCH) != 0) {
+            blockedReasons |= BLOCKED_REASON_DOZE;
+        }
+        if ((blockingMatches & STANDBY_MATCH) != 0) {
+            blockedReasons |= BLOCKED_REASON_APP_STANDBY;
+        }
+        if ((blockingMatches & RESTRICTED_MATCH) != 0) {
+            blockedReasons |= BLOCKED_REASON_RESTRICTED_MODE;
+        }
+        if ((blockingMatches & LOW_POWER_STANDBY_MATCH) != 0) {
+            blockedReasons |= BLOCKED_REASON_LOW_POWER_STANDBY;
+        }
+        if ((blockingMatches & BACKGROUND_MATCH) != 0) {
+            blockedReasons |= BLOCKED_REASON_APP_BACKGROUND;
+        }
+        if ((blockingMatches & (OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)) != 0) {
+            blockedReasons |= BLOCKED_REASON_OEM_DENY;
+        }
+
+        // Metered chains are not enabled by configuration map currently.
+        if ((uidMatch & PENALTY_BOX_USER_MATCH) != 0) {
+            blockedReasons |= BLOCKED_METERED_REASON_USER_RESTRICTED;
+        }
+        if ((uidMatch & PENALTY_BOX_ADMIN_MATCH) != 0) {
+            blockedReasons |= BLOCKED_METERED_REASON_ADMIN_DISABLED;
+        }
+        if ((uidMatch & HAPPY_BOX_MATCH) == 0 && getDataSaverEnabled(dataSaverEnabledMap)) {
+            blockedReasons |= BLOCKED_METERED_REASON_DATA_SAVER;
+        }
+
+        return blockedReasons;
+    }
+
+    /**
+     * Return whether the network is blocked by firewall chains for the given uid.
+     *
+     * Note that {@link #getDataSaverEnabled(IBpfMap)} has a latency before V.
+     *
+     * @param uid The target uid.
+     * @param isNetworkMetered Whether the target network is metered.
+     *
+     * @return True if the network is blocked. Otherwise, false.
+     * @throws ServiceSpecificException if the read fails.
+     *
+     * @hide
+     */
+    public static boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered,
+            IBpfMap<S32, U32> configurationMap,
+            IBpfMap<S32, UidOwnerValue> uidOwnerMap,
+            IBpfMap<S32, U8> dataSaverEnabledMap
+    ) {
+        throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
+
+        final int blockedReasons = getUidNetworkingBlockedReasons(
+                uid,
+                configurationMap,
+                uidOwnerMap,
+                dataSaverEnabledMap);
+        if (isNetworkMetered) {
+            return blockedReasons != BLOCKED_REASON_NONE;
+        } else {
+            return (blockedReasons & ~BLOCKED_METERED_REASON_MASK) != BLOCKED_REASON_NONE;
+        }
+    }
+
+    /**
+     * Get Data Saver enabled or disabled
+     *
+     * Note that before V, the data saver status in bpf is written by ConnectivityService
+     * when receiving {@link ConnectivityManager#ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+     * the status is not synchronized.
+     * On V+, the data saver status is set by platform code when enabling/disabling
+     * data saver, which is synchronized.
+     *
+     * @return whether Data Saver is enabled or disabled.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public static boolean getDataSaverEnabled(IBpfMap<S32, U8> dataSaverEnabledMap) {
+        throwIfPreT("getDataSaverEnabled is not available on pre-T devices");
+
+        try {
+            return dataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val == DATA_SAVER_ENABLED;
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno, "Unable to get data saver: "
+                    + Os.strerror(e.errno));
+        }
+    }
 }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 915ec52..8cf6e04 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -28,6 +28,7 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
+import android.annotation.LongDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresApi;
@@ -70,23 +71,29 @@
 import android.telephony.TelephonyManager;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.LruCache;
 import android.util.Range;
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 
 import libcore.net.event.NetworkEventDispatcher;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
 import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -128,6 +135,12 @@
                 "com.android.net.flags.support_is_uid_networking_blocked";
         static final String BASIC_BACKGROUND_RESTRICTIONS_ENABLED =
                 "com.android.net.flags.basic_background_restrictions_enabled";
+        static final String METERED_NETWORK_FIREWALL_CHAINS =
+                "com.android.net.flags.metered_network_firewall_chains";
+        static final String BLOCKED_REASON_OEM_DENY_CHAINS =
+                "com.android.net.flags.blocked_reason_oem_deny_chains";
+        static final String BLOCKED_REASON_NETWORK_RESTRICTED =
+                "com.android.net.flags.blocked_reason_network_restricted";
     }
 
     /**
@@ -911,6 +924,30 @@
     public static final int BLOCKED_REASON_APP_BACKGROUND = 1 << 6;
 
     /**
+     * Flag to indicate that an app is subject to OEM-specific application restrictions that would
+     * result in its network access being blocked.
+     *
+     * @see #FIREWALL_CHAIN_OEM_DENY_1
+     * @see #FIREWALL_CHAIN_OEM_DENY_2
+     * @see #FIREWALL_CHAIN_OEM_DENY_3
+     * @hide
+     */
+    @FlaggedApi(Flags.BLOCKED_REASON_OEM_DENY_CHAINS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_OEM_DENY = 1 << 7;
+
+    /**
+     * Flag to indicate that an app does not have permission to access the specified network,
+     * for example, because it does not have the {@link android.Manifest.permission#INTERNET}
+     * permission.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.BLOCKED_REASON_NETWORK_RESTRICTED)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_NETWORK_RESTRICTED = 1 << 8;
+
+    /**
      * Flag to indicate that an app is subject to Data saver restrictions that would
      * result in its metered network access being blocked.
      *
@@ -950,6 +987,8 @@
             BLOCKED_REASON_LOCKDOWN_VPN,
             BLOCKED_REASON_LOW_POWER_STANDBY,
             BLOCKED_REASON_APP_BACKGROUND,
+            BLOCKED_REASON_OEM_DENY,
+            BLOCKED_REASON_NETWORK_RESTRICTED,
             BLOCKED_METERED_REASON_DATA_SAVER,
             BLOCKED_METERED_REASON_USER_RESTRICTED,
             BLOCKED_METERED_REASON_ADMIN_DISABLED,
@@ -1068,6 +1107,61 @@
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9;
 
+    /**
+     * Firewall chain for allow list on metered networks
+     *
+     * UIDs added to this chain have access to metered networks, unless they're also in one of the
+     * denylist, {@link #FIREWALL_CHAIN_METERED_DENY_USER},
+     * {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN}
+     *
+     * Note that this chain is used from a separate bpf program that is triggered by iptables and
+     * can not be controlled by {@link ConnectivityManager#setFirewallChainEnabled}.
+     *
+     * @hide
+     */
+    // TODO: Merge this chain with data saver and support setFirewallChainEnabled
+    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_METERED_ALLOW = 10;
+
+    /**
+     * Firewall chain for user-set restrictions on metered networks
+     *
+     * UIDs added to this chain do not have access to metered networks.
+     * UIDs should be added to this chain based on user settings.
+     * To restrict metered network based on admin configuration (e.g. enterprise policies),
+     * {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN} should be used.
+     * This chain corresponds to {@link #BLOCKED_METERED_REASON_USER_RESTRICTED}
+     *
+     * Note that this chain is used from a separate bpf program that is triggered by iptables and
+     * can not be controlled by {@link ConnectivityManager#setFirewallChainEnabled}.
+     *
+     * @hide
+     */
+    // TODO: Support setFirewallChainEnabled to control this chain
+    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_METERED_DENY_USER = 11;
+
+    /**
+     * Firewall chain for admin-set restrictions on metered networks
+     *
+     * UIDs added to this chain do not have access to metered networks.
+     * UIDs should be added to this chain based on admin configuration (e.g. enterprise policies).
+     * To restrict metered network based on user settings, {@link #FIREWALL_CHAIN_METERED_DENY_USER}
+     * should be used.
+     * This chain corresponds to {@link #BLOCKED_METERED_REASON_ADMIN_DISABLED}
+     *
+     * Note that this chain is used from a separate bpf program that is triggered by iptables and
+     * can not be controlled by {@link ConnectivityManager#setFirewallChainEnabled}.
+     *
+     * @hide
+     */
+    // TODO: Support setFirewallChainEnabled to control this chain
+    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_METERED_DENY_ADMIN = 12;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = {
@@ -1079,7 +1173,10 @@
         FIREWALL_CHAIN_BACKGROUND,
         FIREWALL_CHAIN_OEM_DENY_1,
         FIREWALL_CHAIN_OEM_DENY_2,
-        FIREWALL_CHAIN_OEM_DENY_3
+        FIREWALL_CHAIN_OEM_DENY_3,
+        FIREWALL_CHAIN_METERED_ALLOW,
+        FIREWALL_CHAIN_METERED_DENY_USER,
+        FIREWALL_CHAIN_METERED_DENY_ADMIN
     })
     public @interface FirewallChain {}
 
@@ -1117,6 +1214,16 @@
     })
     public @interface FirewallRule {}
 
+    /** @hide */
+    public static final long FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS = 1L;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(flag = true, prefix = "FEATURE_", value = {
+            FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS
+    })
+    public @interface ConnectivityManagerFeature {}
+
     /**
      * A kludge to facilitate static access where a Context pointer isn't available, like in the
      * case of the static set/getProcessDefaultNetwork methods and from the Network class.
@@ -1130,6 +1237,27 @@
     @GuardedBy("mTetheringEventCallbacks")
     private TetheringManager mTetheringManager;
 
+    // Cache of the most recently used NetworkCallback classes (not instances) -> method flags.
+    // 100 is chosen kind arbitrarily as an unlikely number of different types of NetworkCallback
+    // overrides that a process may have, and should generally not be reached (for example, the
+    // system server services.jar has been observed with dexdump to have only 16 when this was
+    // added, and a very large system services app only had 18).
+    // If this number is exceeded, the code will still function correctly, but re-registering
+    // using a network callback class that was used before, but 100+ other classes have been used in
+    // the meantime, will be a bit slower (as slow as the first registration) because
+    // getDeclaredMethodsFlag must re-examine the callback class to determine what methods it
+    // overrides.
+    private static final LruCache<Class<? extends NetworkCallback>, Integer> sMethodFlagsCache =
+            new LruCache<>(100);
+
+    private final Object mEnabledConnectivityManagerFeaturesLock = new Object();
+    // mEnabledConnectivityManagerFeatures is lazy-loaded in this ConnectivityManager instance, but
+    // fetched from ConnectivityService, where it is loaded in ConnectivityService startup, so it
+    // should have consistent values.
+    @GuardedBy("sEnabledConnectivityManagerFeaturesLock")
+    @ConnectivityManagerFeature
+    private Long mEnabledConnectivityManagerFeatures = null;
+
     private TetheringManager getTetheringManager() {
         synchronized (mTetheringEventCallbacks) {
             if (mTetheringManager == null) {
@@ -3887,6 +4015,55 @@
      */
     public static class NetworkCallback {
         /**
+         * Bitmask of method flags with all flags set.
+         * @hide
+         */
+        public static final int DECLARED_METHODS_ALL = ~0;
+
+        /**
+         * Bitmask of method flags with no flag set.
+         * @hide
+         */
+        public static final int DECLARED_METHODS_NONE = 0;
+
+        // Tracks whether an instance was created via reflection without calling the constructor.
+        private final boolean mConstructorWasCalled;
+
+        /**
+         * Annotation for NetworkCallback methods to verify filtering is configured properly.
+         *
+         * This is only used in tests to ensure that tests fail when a new callback is added, or
+         * callbacks are modified, without updating
+         * {@link NetworkCallbackMethodsHolder#NETWORK_CB_METHODS} properly.
+         * @hide
+         */
+        @Retention(RetentionPolicy.RUNTIME)
+        @Target(ElementType.METHOD)
+        @VisibleForTesting
+        public @interface FilteredCallback {
+            /**
+             * The NetworkCallback.METHOD_* ID of this method.
+             */
+            int methodId();
+
+            /**
+             * The ConnectivityManager.CALLBACK_* message that this method is directly called by.
+             *
+             * If this method is not called by any message, this should be
+             * {@link #CALLBACK_TRANSITIVE_CALLS_ONLY}.
+             */
+            int calledByCallbackId();
+
+            /**
+             * If this method may call other NetworkCallback methods, an array of methods it calls.
+             *
+             * Only direct calls (not transitive calls) should be included. The IDs must be
+             * NetworkCallback.METHOD_* IDs.
+             */
+            int[] mayCall() default {};
+        }
+
+        /**
          * No flags associated with this callback.
          * @hide
          */
@@ -3949,6 +4126,7 @@
                 throw new IllegalArgumentException("Invalid flags");
             }
             mFlags = flags;
+            mConstructorWasCalled = true;
         }
 
         /**
@@ -3966,7 +4144,9 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONPRECHECK, calledByCallbackId = CALLBACK_PRECHECK)
         public void onPreCheck(@NonNull Network network) {}
+        private static final int METHOD_ONPRECHECK = 1;
 
         /**
          * Called when the framework connects and has declared a new network ready for use.
@@ -3981,6 +4161,13 @@
          * @param blocked Whether access to the {@link Network} is blocked due to system policy.
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONAVAILABLE_5ARGS,
+                calledByCallbackId = CALLBACK_AVAILABLE,
+                // If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
+                // needs to be updated too.
+                mayCall = { METHOD_ONAVAILABLE_4ARGS,
+                        METHOD_ONLOCALNETWORKINFOCHANGED,
+                        METHOD_ONBLOCKEDSTATUSCHANGED_INT })
         public final void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
                 @NonNull LinkProperties linkProperties,
@@ -3993,6 +4180,7 @@
             if (null != localInfo) onLocalNetworkInfoChanged(network, localInfo);
             onBlockedStatusChanged(network, blocked);
         }
+        private static final int METHOD_ONAVAILABLE_5ARGS = 2;
 
         /**
          * Legacy variant of onAvailable that takes a boolean blocked reason.
@@ -4005,6 +4193,15 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONAVAILABLE_4ARGS,
+                calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY,
+                // If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
+                // needs to be updated too.
+                mayCall = { METHOD_ONAVAILABLE_1ARG,
+                        METHOD_ONNETWORKSUSPENDED,
+                        METHOD_ONCAPABILITIESCHANGED,
+                        METHOD_ONLINKPROPERTIESCHANGED
+                })
         public void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
                 @NonNull LinkProperties linkProperties,
@@ -4018,6 +4215,7 @@
             onLinkPropertiesChanged(network, linkProperties);
             // No call to onBlockedStatusChanged here. That is done by the caller.
         }
+        private static final int METHOD_ONAVAILABLE_4ARGS = 3;
 
         /**
          * Called when the framework connects and has declared a new network ready for use.
@@ -4048,7 +4246,10 @@
          *
          * @param network The {@link Network} of the satisfying network.
          */
+        @FilteredCallback(methodId = METHOD_ONAVAILABLE_1ARG,
+                calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY)
         public void onAvailable(@NonNull Network network) {}
+        private static final int METHOD_ONAVAILABLE_1ARG = 4;
 
         /**
          * Called when the network is about to be lost, typically because there are no outstanding
@@ -4067,7 +4268,9 @@
          *                    connected for graceful handover; note that the network may still
          *                    suffer a hard loss at any time.
          */
+        @FilteredCallback(methodId = METHOD_ONLOSING, calledByCallbackId = CALLBACK_LOSING)
         public void onLosing(@NonNull Network network, int maxMsToLive) {}
+        private static final int METHOD_ONLOSING = 5;
 
         /**
          * Called when a network disconnects or otherwise no longer satisfies this request or
@@ -4088,7 +4291,9 @@
          *
          * @param network The {@link Network} lost.
          */
+        @FilteredCallback(methodId = METHOD_ONLOST, calledByCallbackId = CALLBACK_LOST)
         public void onLost(@NonNull Network network) {}
+        private static final int METHOD_ONLOST = 6;
 
         /**
          * Called if no network is found within the timeout time specified in
@@ -4098,7 +4303,9 @@
          * {@link NetworkRequest} will have already been removed and released, as if
          * {@link #unregisterNetworkCallback(NetworkCallback)} had been called.
          */
+        @FilteredCallback(methodId = METHOD_ONUNAVAILABLE, calledByCallbackId = CALLBACK_UNAVAIL)
         public void onUnavailable() {}
+        private static final int METHOD_ONUNAVAILABLE = 7;
 
         /**
          * Called when the network corresponding to this request changes capabilities but still
@@ -4115,8 +4322,11 @@
          * @param networkCapabilities The new {@link NetworkCapabilities} for this
          *                            network.
          */
+        @FilteredCallback(methodId = METHOD_ONCAPABILITIESCHANGED,
+                calledByCallbackId = CALLBACK_CAP_CHANGED)
         public void onCapabilitiesChanged(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities) {}
+        private static final int METHOD_ONCAPABILITIESCHANGED = 8;
 
         /**
          * Called when the network corresponding to this request changes {@link LinkProperties}.
@@ -4131,8 +4341,11 @@
          * @param network The {@link Network} whose link properties have changed.
          * @param linkProperties The new {@link LinkProperties} for this network.
          */
+        @FilteredCallback(methodId = METHOD_ONLINKPROPERTIESCHANGED,
+                calledByCallbackId = CALLBACK_IP_CHANGED)
         public void onLinkPropertiesChanged(@NonNull Network network,
                 @NonNull LinkProperties linkProperties) {}
+        private static final int METHOD_ONLINKPROPERTIESCHANGED = 9;
 
         /**
          * Called when there is a change in the {@link LocalNetworkInfo} for this network.
@@ -4144,8 +4357,11 @@
          * @param localNetworkInfo the new {@link LocalNetworkInfo} for this network.
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONLOCALNETWORKINFOCHANGED,
+                calledByCallbackId = CALLBACK_LOCAL_NETWORK_INFO_CHANGED)
         public void onLocalNetworkInfoChanged(@NonNull Network network,
                 @NonNull LocalNetworkInfo localNetworkInfo) {}
+        private static final int METHOD_ONLOCALNETWORKINFOCHANGED = 10;
 
         /**
          * Called when the network the framework connected to for this request suspends data
@@ -4164,7 +4380,10 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONNETWORKSUSPENDED,
+                calledByCallbackId = CALLBACK_SUSPENDED)
         public void onNetworkSuspended(@NonNull Network network) {}
+        private static final int METHOD_ONNETWORKSUSPENDED = 11;
 
         /**
          * Called when the network the framework connected to for this request
@@ -4178,7 +4397,9 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONNETWORKRESUMED, calledByCallbackId = CALLBACK_RESUMED)
         public void onNetworkResumed(@NonNull Network network) {}
+        private static final int METHOD_ONNETWORKRESUMED = 12;
 
         /**
          * Called when access to the specified network is blocked or unblocked.
@@ -4191,7 +4412,10 @@
          * @param network The {@link Network} whose blocked status has changed.
          * @param blocked The blocked status of this {@link Network}.
          */
+        @FilteredCallback(methodId = METHOD_ONBLOCKEDSTATUSCHANGED_BOOL,
+                calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY)
         public void onBlockedStatusChanged(@NonNull Network network, boolean blocked) {}
+        private static final int METHOD_ONBLOCKEDSTATUSCHANGED_BOOL = 13;
 
         /**
          * Called when access to the specified network is blocked or unblocked, or the reason for
@@ -4209,10 +4433,14 @@
          * @param blocked The blocked status of this {@link Network}.
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONBLOCKEDSTATUSCHANGED_INT,
+                calledByCallbackId = CALLBACK_BLK_CHANGED,
+                mayCall = { METHOD_ONBLOCKEDSTATUSCHANGED_BOOL })
         @SystemApi(client = MODULE_LIBRARIES)
         public void onBlockedStatusChanged(@NonNull Network network, @BlockedReason int blocked) {
             onBlockedStatusChanged(network, blocked != 0);
         }
+        private static final int METHOD_ONBLOCKEDSTATUSCHANGED_INT = 14;
 
         private NetworkRequest networkRequest;
         private final int mFlags;
@@ -4240,6 +4468,7 @@
         }
     }
 
+    private static final int CALLBACK_TRANSITIVE_CALLS_ONLY     = 0;
     /** @hide */
     public static final int CALLBACK_PRECHECK                   = 1;
     /** @hide */
@@ -4264,10 +4493,13 @@
     public static final int CALLBACK_BLK_CHANGED                = 11;
     /** @hide */
     public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
+    // When adding new IDs, note CallbackQueue assumes callback IDs are at most 16 bits.
+
 
     /** @hide */
     public static String getCallbackName(int whichCallback) {
         switch (whichCallback) {
+            case CALLBACK_TRANSITIVE_CALLS_ONLY: return "CALLBACK_TRANSITIVE_CALLS_ONLY";
             case CALLBACK_PRECHECK:     return "CALLBACK_PRECHECK";
             case CALLBACK_AVAILABLE:    return "CALLBACK_AVAILABLE";
             case CALLBACK_LOSING:       return "CALLBACK_LOSING";
@@ -4285,6 +4517,68 @@
         }
     }
 
+    /** @hide */
+    @VisibleForTesting
+    public static class NetworkCallbackMethod {
+        @NonNull
+        public final String mName;
+        @NonNull
+        public final Class<?>[] mParameterTypes;
+        // Bitmask of CALLBACK_* that may transitively call this method.
+        public final int mCallbacksCallingThisMethod;
+
+        public NetworkCallbackMethod(@NonNull String name, @NonNull Class<?>[] parameterTypes,
+                int callbacksCallingThisMethod) {
+            mName = name;
+            mParameterTypes = parameterTypes;
+            mCallbacksCallingThisMethod = callbacksCallingThisMethod;
+        }
+    }
+
+    // Holder class for the list of NetworkCallbackMethod. This ensures the list is only created
+    // once on first usage, and not just on ConnectivityManager class initialization.
+    /** @hide */
+    @VisibleForTesting
+    public static class NetworkCallbackMethodsHolder {
+        public static final NetworkCallbackMethod[] NETWORK_CB_METHODS =
+                new NetworkCallbackMethod[] {
+                        method("onPreCheck", 1 << CALLBACK_PRECHECK, Network.class),
+                        // Note the final overload of onAvailable is not included, since it cannot
+                        // match any overridden method.
+                        method("onAvailable", 1 << CALLBACK_AVAILABLE, Network.class),
+                        method("onAvailable", 1 << CALLBACK_AVAILABLE,
+                                Network.class, NetworkCapabilities.class,
+                                LinkProperties.class, boolean.class),
+                        method("onLosing", 1 << CALLBACK_LOSING, Network.class, int.class),
+                        method("onLost", 1 << CALLBACK_LOST, Network.class),
+                        method("onUnavailable", 1 << CALLBACK_UNAVAIL),
+                        method("onCapabilitiesChanged",
+                                1 << CALLBACK_CAP_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, NetworkCapabilities.class),
+                        method("onLinkPropertiesChanged",
+                                1 << CALLBACK_IP_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, LinkProperties.class),
+                        method("onLocalNetworkInfoChanged",
+                                1 << CALLBACK_LOCAL_NETWORK_INFO_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, LocalNetworkInfo.class),
+                        method("onNetworkSuspended",
+                                1 << CALLBACK_SUSPENDED | 1 << CALLBACK_AVAILABLE, Network.class),
+                        method("onNetworkResumed",
+                                1 << CALLBACK_RESUMED, Network.class),
+                        method("onBlockedStatusChanged",
+                                1 << CALLBACK_BLK_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, boolean.class),
+                        method("onBlockedStatusChanged",
+                                1 << CALLBACK_BLK_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, int.class),
+                };
+
+        private static NetworkCallbackMethod method(
+                String name, int callbacksCallingThisMethod, Class<?>... args) {
+            return new NetworkCallbackMethod(name, args, callbacksCallingThisMethod);
+        }
+    }
+
     private static class CallbackHandler extends Handler {
         private static final String TAG = "ConnectivityManager.CallbackHandler";
         private static final boolean DBG = false;
@@ -4404,6 +4698,14 @@
         if (reqType != TRACK_DEFAULT && reqType != TRACK_SYSTEM_DEFAULT && need == null) {
             throw new IllegalArgumentException("null NetworkCapabilities");
         }
+
+
+        final boolean useDeclaredMethods = isFeatureEnabled(
+                FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS);
+        // Set all bits if the feature is disabled
+        int declaredMethodsFlag = useDeclaredMethods
+                ? tryGetDeclaredMethodsFlag(callback)
+                : NetworkCallback.DECLARED_METHODS_ALL;
         final NetworkRequest request;
         final String callingPackageName = mContext.getOpPackageName();
         try {
@@ -4420,11 +4722,12 @@
                 if (reqType == LISTEN) {
                     request = mService.listenForNetwork(
                             need, messenger, binder, callbackFlags, callingPackageName,
-                            getAttributionTag());
+                            getAttributionTag(), declaredMethodsFlag);
                 } else {
                     request = mService.requestNetwork(
                             asUid, need, reqType.ordinal(), messenger, timeoutMs, binder,
-                            legacyType, callbackFlags, callingPackageName, getAttributionTag());
+                            legacyType, callbackFlags, callingPackageName, getAttributionTag(),
+                            declaredMethodsFlag);
                 }
                 if (request != null) {
                     sCallbacks.put(request, callback);
@@ -4439,6 +4742,122 @@
         return request;
     }
 
+    private int tryGetDeclaredMethodsFlag(@NonNull NetworkCallback cb) {
+        if (!cb.mConstructorWasCalled) {
+            // Do not use the optimization if the callback was created via reflection or mocking,
+            // as for example with dexmaker-mockito-inline methods will be instrumented without
+            // using subclasses. This does not catch all cases as it is still possible to call the
+            // constructor when creating mocks, but by default constructors are not called in that
+            // case.
+            return NetworkCallback.DECLARED_METHODS_ALL;
+        }
+        try {
+            return getDeclaredMethodsFlag(cb.getClass());
+        } catch (LinkageError e) {
+            // This may happen if some methods reference inaccessible classes in their arguments
+            // (for example b/261807130).
+            Log.w(TAG, "Could not get methods from NetworkCallback class", e);
+            // Fall through
+        } catch (Throwable e) {
+            // Log.wtf would be best but this is in app process, so the TerribleFailureHandler may
+            // have unknown effects, possibly crashing the app (default behavior on eng builds or
+            // if the WTF_IS_FATAL setting is set).
+            Log.e(TAG, "Unexpected error while getting methods from NetworkCallback class", e);
+            // Fall through
+        }
+        return NetworkCallback.DECLARED_METHODS_ALL;
+    }
+
+    private static int getDeclaredMethodsFlag(@NonNull Class<? extends NetworkCallback> clazz) {
+        final Integer cachedFlags = sMethodFlagsCache.get(clazz);
+        // As this is not synchronized, it is possible that this method will calculate the
+        // flags for a given class multiple times, but that is fine. LruCache itself is thread-safe.
+        if (cachedFlags != null) {
+            return cachedFlags;
+        }
+
+        int flag = 0;
+        // This uses getMethods instead of getDeclaredMethods, to make sure that if A overrides B
+        // that overrides NetworkCallback, A.getMethods also returns methods declared by B.
+        for (Method classMethod : clazz.getMethods()) {
+            final Class<?> declaringClass = classMethod.getDeclaringClass();
+            if (declaringClass == NetworkCallback.class) {
+                // The callback is as defined by NetworkCallback and not overridden
+                continue;
+            }
+            if (declaringClass == Object.class) {
+                // Optimization: no need to try to match callbacks for methods declared by Object
+                continue;
+            }
+            flag |= getCallbackIdsCallingThisMethod(classMethod);
+        }
+
+        if (flag == 0) {
+            // dexmaker-mockito-inline (InlineDexmakerMockMaker), for example for mockito-extended,
+            // modifies bytecode of classes in-place to add hooks instead of creating subclasses,
+            // which would not be detected. When no method is found, fall back to enabling callbacks
+            // for all methods.
+            // This will not catch the case where both NetworkCallback bytecode is modified and a
+            // subclass of NetworkCallback that has some overridden methods are used. But this kind
+            // of bytecode injection is only possible in debuggable processes, with a JVMTI debug
+            // agent attached, so it should not cause real issues.
+            // There may be legitimate cases where an empty callback is filed with no method
+            // overridden, for example requestNetwork(requestForCell, new NetworkCallback()) which
+            // would ensure that one cell network stays up. But there is no way to differentiate
+            // such NetworkCallbacks from a mock that called the constructor, so this code will
+            // register the callback with DECLARED_METHODS_ALL and turn off the optimization in that
+            // case. Apps are not expected to do this often anyway since the usefulness is very
+            // limited.
+            flag = NetworkCallback.DECLARED_METHODS_ALL;
+        }
+        sMethodFlagsCache.put(clazz, flag);
+        return flag;
+    }
+
+    /**
+     * Find out which of the base methods in NetworkCallback will call this method.
+     *
+     * For example, in the case of onLinkPropertiesChanged, this will be
+     * (1 << CALLBACK_IP_CHANGED) | (1 << CALLBACK_AVAILABLE).
+     */
+    private static int getCallbackIdsCallingThisMethod(@NonNull Method method) {
+        for (NetworkCallbackMethod baseMethod : NetworkCallbackMethodsHolder.NETWORK_CB_METHODS) {
+            if (!baseMethod.mName.equals(method.getName())) {
+                continue;
+            }
+            Class<?>[] methodParams = method.getParameterTypes();
+
+            // As per JLS 8.4.8.1., a method m1 must have a subsignature of method m2 to override
+            // it. And as per JLS 8.4.2, this means the erasure of the signature of m2 must be the
+            // same as the signature of m1. Since type erasure is done at compile time, with
+            // reflection the erased types are already observed, so the (erased) parameter types
+            // must be equal.
+            // So for example a method that is identical to a NetworkCallback method, except with
+            // one parameter being a subclass of the parameter in the original method, will never
+            // be called since it is not an override (the erasure of the arguments are not the same)
+            // Therefore, the method is an override only if methodParams is exactly equal to
+            // the base method's parameter types.
+            if (Arrays.equals(baseMethod.mParameterTypes, methodParams)) {
+                return baseMethod.mCallbacksCallingThisMethod;
+            }
+        }
+        return 0;
+    }
+
+    private boolean isFeatureEnabled(@ConnectivityManagerFeature long connectivityManagerFeature) {
+        synchronized (mEnabledConnectivityManagerFeaturesLock) {
+            if (mEnabledConnectivityManagerFeatures == null) {
+                try {
+                    mEnabledConnectivityManagerFeatures =
+                            mService.getEnabledConnectivityManagerFeatures();
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
+            }
+            return (mEnabledConnectivityManagerFeatures & connectivityManagerFeature) != 0;
+        }
+    }
+
     private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, NetworkCallback callback,
             int timeoutMs, NetworkRequest.Type reqType, int legacyType, CallbackHandler handler) {
         return sendRequestForNetwork(Process.INVALID_UID, need, callback, timeoutMs, reqType,
@@ -6065,7 +6484,7 @@
     })
     public void addUidToMeteredNetworkAllowList(final int uid) {
         try {
-            mService.updateMeteredNetworkAllowList(uid, true /* add */);
+            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_ALLOW);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -6088,7 +6507,7 @@
     })
     public void removeUidFromMeteredNetworkAllowList(final int uid) {
         try {
-            mService.updateMeteredNetworkAllowList(uid, false /* remove */);
+            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_DENY);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -6098,10 +6517,17 @@
      * Adds the specified UID to the list of UIDs that are not allowed to use background data on
      * metered networks. Takes precedence over {@link #addUidToMeteredNetworkAllowList}.
      *
+     * On V+, {@link #setUidFirewallRule} should be used with
+     * {@link #FIREWALL_CHAIN_METERED_DENY_USER} or {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN}
+     * based on the reason so that users can receive {@link #BLOCKED_METERED_REASON_USER_RESTRICTED}
+     * or {@link #BLOCKED_METERED_REASON_ADMIN_DISABLED}, respectively.
+     * This API always uses {@link #FIREWALL_CHAIN_METERED_DENY_USER}.
+     *
      * @param uid uid of target app
      * @throws IllegalStateException if updating deny list failed.
      * @hide
      */
+    // TODO(b/332649177): Deprecate this API after V
     @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_SETTINGS,
@@ -6110,7 +6536,7 @@
     })
     public void addUidToMeteredNetworkDenyList(final int uid) {
         try {
-            mService.updateMeteredNetworkDenyList(uid, true /* add */);
+            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_DENY);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -6121,10 +6547,17 @@
      * networks if background data is not restricted. The deny list takes precedence over the
      * allow list.
      *
+     * On V+, {@link #setUidFirewallRule} should be used with
+     * {@link #FIREWALL_CHAIN_METERED_DENY_USER} or {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN}
+     * based on the reason so that users can receive {@link #BLOCKED_METERED_REASON_USER_RESTRICTED}
+     * or {@link #BLOCKED_METERED_REASON_ADMIN_DISABLED}, respectively.
+     * This API always uses {@link #FIREWALL_CHAIN_METERED_DENY_USER}.
+     *
      * @param uid uid of target app
      * @throws IllegalStateException if updating deny list failed.
      * @hide
      */
+    // TODO(b/332649177): Deprecate this API after V
     @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_SETTINGS,
@@ -6133,7 +6566,7 @@
     })
     public void removeUidFromMeteredNetworkDenyList(final int uid) {
         try {
-            mService.updateMeteredNetworkDenyList(uid, false /* remove */);
+            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_ALLOW);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -6191,6 +6624,10 @@
     /**
      * Enables or disables the specified firewall chain.
      *
+     * Note that metered firewall chains can not be controlled by this API.
+     * See {@link #FIREWALL_CHAIN_METERED_ALLOW}, {@link #FIREWALL_CHAIN_METERED_DENY_USER}, and
+     * {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN} for more detail.
+     *
      * @param chain target chain.
      * @param enable whether the chain should be enabled.
      * @throws UnsupportedOperationException if called on pre-T devices.
@@ -6284,15 +6721,16 @@
     @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
     public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
         if (!SdkLevel.isAtLeastU()) {
-            Log.wtf(TAG, "isUidNetworkingBlocked is not supported on pre-U devices");
+            throw new IllegalStateException(
+                    "isUidNetworkingBlocked is not supported on pre-U devices");
         }
-        final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
+        final NetworkStackBpfNetMaps reader = NetworkStackBpfNetMaps.getInstance();
         // Note that before V, the data saver status in bpf is written by ConnectivityService
         // when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
         // the status is not synchronized.
         // On V+, the data saver status is set by platform code when enabling/disabling
         // data saver, which is synchronized.
-        return reader.isUidNetworkingBlocked(uid, isNetworkMetered, reader.getDataSaverEnabled());
+        return reader.isUidNetworkingBlocked(uid, isNetworkMetered);
     }
 
     /** @hide */
@@ -6304,21 +6742,11 @@
         }
     }
 
-    private static final Object sRoutingCoordinatorManagerLock = new Object();
-    @GuardedBy("sRoutingCoordinatorManagerLock")
-    private static RoutingCoordinatorManager sRoutingCoordinatorManager = null;
     /** @hide */
     @RequiresApi(Build.VERSION_CODES.S)
-    public RoutingCoordinatorManager getRoutingCoordinatorManager() {
+    public IBinder getRoutingCoordinatorService() {
         try {
-            synchronized (sRoutingCoordinatorManagerLock) {
-                if (null == sRoutingCoordinatorManager) {
-                    sRoutingCoordinatorManager = new RoutingCoordinatorManager(mContext,
-                            IRoutingCoordinator.Stub.asInterface(
-                                    mService.getRoutingCoordinatorService()));
-                }
-                return sRoutingCoordinatorManager;
-            }
+            return mService.getRoutingCoordinatorService();
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index d3a02b9..988cc92 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -153,7 +153,8 @@
 
     NetworkRequest requestNetwork(int uid, in NetworkCapabilities networkCapabilities, int reqType,
             in Messenger messenger, int timeoutSec, in IBinder binder, int legacy,
-            int callbackFlags, String callingPackageName, String callingAttributionTag);
+            int callbackFlags, String callingPackageName, String callingAttributionTag,
+            int declaredMethodsFlag);
 
     NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities,
             in PendingIntent operation, String callingPackageName, String callingAttributionTag);
@@ -162,7 +163,7 @@
 
     NetworkRequest listenForNetwork(in NetworkCapabilities networkCapabilities,
             in Messenger messenger, in IBinder binder, int callbackFlags, String callingPackageName,
-            String callingAttributionTag);
+            String callingAttributionTag, int declaredMethodsFlag);
 
     void pendingListenForNetwork(in NetworkCapabilities networkCapabilities,
             in PendingIntent operation, String callingPackageName,
@@ -242,10 +243,6 @@
 
     void setDataSaverEnabled(boolean enable);
 
-    void updateMeteredNetworkAllowList(int uid, boolean add);
-
-    void updateMeteredNetworkDenyList(int uid, boolean add);
-
     void setUidFirewallRule(int chain, int uid, int rule);
 
     int getUidFirewallRule(int chain, int uid);
@@ -263,4 +260,6 @@
     void setTestLowTcpPollingTimerForKeepalive(long timeMs);
 
     IBinder getRoutingCoordinatorService();
+
+    long getEnabledConnectivityManagerFeatures();
 }
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 84a0d29..6a14bde 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -36,9 +36,11 @@
 import android.os.Process;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.Log;
 import android.util.Range;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BitUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
@@ -134,6 +136,8 @@
                 "com.android.net.flags.request_restricted_wifi";
         static final String SUPPORT_TRANSPORT_SATELLITE =
                 "com.android.net.flags.support_transport_satellite";
+        static final String NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED =
+                "com.android.net.flags.net_capability_not_bandwidth_constrained";
     }
 
     /**
@@ -458,6 +462,7 @@
             NET_CAPABILITY_PRIORITIZE_LATENCY,
             NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
             NET_CAPABILITY_LOCAL_NETWORK,
+            NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED,
     })
     public @interface NetCapability { }
 
@@ -740,7 +745,26 @@
     @FlaggedApi(Flags.FLAG_NET_CAPABILITY_LOCAL_NETWORK)
     public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
 
-    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
+    /**
+     * Indicates that this is not a bandwidth-constrained network.
+     *
+     * Starting from {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this capability is by default
+     * set in {@link NetworkRequest}s and true for most networks.
+     *
+     * If a network lacks this capability, it is bandwidth-constrained. Bandwidth constrained
+     * networks cannot support high-bandwidth data transfers and applications that request and use
+     * them must ensure that they limit bandwidth usage to below the values returned by
+     * {@link #getLinkDownstreamBandwidthKbps()} and {@link #getLinkUpstreamBandwidthKbps()} and
+     * limit the frequency of their network usage. If applications perform high-bandwidth data
+     * transfers on constrained networks or perform network access too frequently, the system may
+     * block the app's access to the network. The system may take other measures to reduce network
+     * usage on constrained networks, such as disabling network access to apps that are not in the
+     * foreground.
+     */
+    @FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+    public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
+
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 
     // Set all bits up to the MAX_NET_CAPABILITY-th bit
     private static final long ALL_VALID_CAPABILITIES = (2L << MAX_NET_CAPABILITY) - 1;
@@ -749,22 +773,22 @@
      * Network capabilities that are expected to be mutable, i.e., can change while a particular
      * network is connected.
      */
-    private static final long MUTABLE_CAPABILITIES = BitUtils.packBitList(
+    private static final long MUTABLE_CAPABILITIES =
             // TRUSTED can change when user explicitly connects to an untrusted network in Settings.
             // http://b/18206275
-            NET_CAPABILITY_TRUSTED,
-            NET_CAPABILITY_VALIDATED,
-            NET_CAPABILITY_CAPTIVE_PORTAL,
-            NET_CAPABILITY_NOT_ROAMING,
-            NET_CAPABILITY_FOREGROUND,
-            NET_CAPABILITY_NOT_CONGESTED,
-            NET_CAPABILITY_NOT_SUSPENDED,
-            NET_CAPABILITY_PARTIAL_CONNECTIVITY,
-            NET_CAPABILITY_TEMPORARILY_NOT_METERED,
-            NET_CAPABILITY_NOT_VCN_MANAGED,
+            (1L << NET_CAPABILITY_TRUSTED) |
+            (1L << NET_CAPABILITY_VALIDATED) |
+            (1L << NET_CAPABILITY_CAPTIVE_PORTAL) |
+            (1L << NET_CAPABILITY_NOT_ROAMING) |
+            (1L << NET_CAPABILITY_FOREGROUND) |
+            (1L << NET_CAPABILITY_NOT_CONGESTED) |
+            (1L << NET_CAPABILITY_NOT_SUSPENDED) |
+            (1L << NET_CAPABILITY_PARTIAL_CONNECTIVITY) |
+            (1L << NET_CAPABILITY_TEMPORARILY_NOT_METERED) |
+            (1L << NET_CAPABILITY_NOT_VCN_MANAGED) |
             // The value of NET_CAPABILITY_HEAD_UNIT is 32, which cannot use int to do bit shift,
             // otherwise there will be an overflow. Use long to do bit shift instead.
-            NET_CAPABILITY_HEAD_UNIT);
+            (1L << NET_CAPABILITY_HEAD_UNIT);
 
     /**
      * Network capabilities that are not allowed in NetworkRequests. This exists because the
@@ -784,10 +808,17 @@
     /**
      * Capabilities that are set by default when the object is constructed.
      */
-    private static final long DEFAULT_CAPABILITIES = BitUtils.packBitList(
-            NET_CAPABILITY_NOT_RESTRICTED,
-            NET_CAPABILITY_TRUSTED,
-            NET_CAPABILITY_NOT_VPN);
+    private static final long DEFAULT_CAPABILITIES;
+    static {
+        long defaultCapabilities =
+                (1L << NET_CAPABILITY_NOT_RESTRICTED)
+                | (1L << NET_CAPABILITY_TRUSTED)
+                | (1L << NET_CAPABILITY_NOT_VPN);
+        if (SdkLevel.isAtLeastV()) {
+            defaultCapabilities |= (1L << NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+        }
+        DEFAULT_CAPABILITIES = defaultCapabilities;
+    }
 
     /**
      * Capabilities that are managed by ConnectivityService.
@@ -795,11 +826,10 @@
      */
     @VisibleForTesting
     public static final long CONNECTIVITY_MANAGED_CAPABILITIES =
-            BitUtils.packBitList(
-                    NET_CAPABILITY_VALIDATED,
-                    NET_CAPABILITY_CAPTIVE_PORTAL,
-                    NET_CAPABILITY_FOREGROUND,
-                    NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+            (1L << NET_CAPABILITY_VALIDATED) |
+            (1L << NET_CAPABILITY_CAPTIVE_PORTAL) |
+            (1L << NET_CAPABILITY_FOREGROUND) |
+            (1L << NET_CAPABILITY_PARTIAL_CONNECTIVITY);
 
     /**
      * Capabilities that are allowed for all test networks. This list must be set so that it is safe
@@ -808,15 +838,16 @@
      * IMS, SUPL, etc.
      */
     private static final long TEST_NETWORKS_ALLOWED_CAPABILITIES =
-            BitUtils.packBitList(
-            NET_CAPABILITY_NOT_METERED,
-            NET_CAPABILITY_TEMPORARILY_NOT_METERED,
-            NET_CAPABILITY_NOT_RESTRICTED,
-            NET_CAPABILITY_NOT_VPN,
-            NET_CAPABILITY_NOT_ROAMING,
-            NET_CAPABILITY_NOT_CONGESTED,
-            NET_CAPABILITY_NOT_SUSPENDED,
-            NET_CAPABILITY_NOT_VCN_MANAGED);
+            (1L << NET_CAPABILITY_NOT_METERED) |
+            (1L << NET_CAPABILITY_TEMPORARILY_NOT_METERED) |
+            (1L << NET_CAPABILITY_NOT_RESTRICTED) |
+            (1L << NET_CAPABILITY_NOT_VPN) |
+            (1L << NET_CAPABILITY_NOT_ROAMING) |
+            (1L << NET_CAPABILITY_NOT_CONGESTED) |
+            (1L << NET_CAPABILITY_NOT_SUSPENDED) |
+            (1L << NET_CAPABILITY_NOT_VCN_MANAGED) |
+            (1L << NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+
 
     /**
      * Extra allowed capabilities for test networks that do not have TRANSPORT_CELLULAR. Test
@@ -824,7 +855,9 @@
      * the risk of being used by running apps.
      */
     private static final long TEST_NETWORKS_EXTRA_ALLOWED_CAPABILITIES_ON_NON_CELL =
-            BitUtils.packBitList(NET_CAPABILITY_CBS, NET_CAPABILITY_DUN, NET_CAPABILITY_RCS);
+            (1L << NET_CAPABILITY_CBS) |
+            (1L << NET_CAPABILITY_DUN) |
+            (1L << NET_CAPABILITY_RCS);
 
     /**
      * Adds the given capability to this {@code NetworkCapability} instance.
@@ -843,7 +876,10 @@
         // If the given capability was previously added to the list of forbidden capabilities
         // then the capability will also be removed from the list of forbidden capabilities.
         // TODO: Add forbidden capabilities to the public API
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "addCapability is called with invalid capability: " + capability);
+            return this;
+        }
         mNetworkCapabilities |= 1L << capability;
         // remove from forbidden capability list
         mForbiddenNetworkCapabilities &= ~(1L << capability);
@@ -864,7 +900,10 @@
      * @hide
      */
     public void addForbiddenCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "addForbiddenCapability is called with invalid capability: " + capability);
+            return;
+        }
         mForbiddenNetworkCapabilities |= 1L << capability;
         mNetworkCapabilities &= ~(1L << capability);  // remove from requested capabilities
     }
@@ -878,7 +917,10 @@
      * @hide
      */
     public @NonNull NetworkCapabilities removeCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG, "removeCapability is called with invalid capability: " + capability);
+            return this;
+        }
         final long mask = ~(1L << capability);
         mNetworkCapabilities &= mask;
         return this;
@@ -893,7 +935,11 @@
      * @hide
      */
     public @NonNull NetworkCapabilities removeForbiddenCapability(@NetCapability int capability) {
-        checkValidCapability(capability);
+        if (!isValidCapability(capability)) {
+            Log.e(TAG,
+                    "removeForbiddenCapability is called with invalid capability: " + capability);
+            return this;
+        }
         mForbiddenNetworkCapabilities &= ~(1L << capability);
         return this;
     }
@@ -1174,7 +1220,7 @@
      * @hide
      */
     public void maybeMarkCapabilitiesRestricted() {
-        if (NetworkCapabilitiesUtils.inferRestrictedCapability(this)) {
+        if (NetworkCapabilitiesUtils.inferRestrictedCapability(mNetworkCapabilities)) {
             removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
         }
     }
@@ -1368,12 +1414,11 @@
      * Allowed transports on an unrestricted test network (in addition to TRANSPORT_TEST).
      */
     private static final long UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS =
-            BitUtils.packBitList(
-                    TRANSPORT_TEST,
-                    // Test eth networks are created with EthernetManager#setIncludeTestInterfaces
-                    TRANSPORT_ETHERNET,
-                    // Test VPN networks can be created but their UID ranges must be empty.
-                    TRANSPORT_VPN);
+            (1L << TRANSPORT_TEST) |
+            // Test eth networks are created with EthernetManager#setIncludeTestInterfaces
+            (1L << TRANSPORT_ETHERNET) |
+            // Test VPN networks can be created but their UID ranges must be empty.
+            (1L << TRANSPORT_VPN);
 
     /**
      * Adds the given transport type to this {@code NetworkCapability} instance.
@@ -1775,8 +1820,7 @@
                 // use the same specifier, TelephonyNetworkSpecifier.
                 && mTransportTypes != (1L << TRANSPORT_TEST)
                 && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1
-                && (mTransportTypes & ~(1L << TRANSPORT_TEST))
-                != (1 << TRANSPORT_CELLULAR | 1 << TRANSPORT_SATELLITE)) {
+                && !specifierAcceptableForMultipleTransports(mTransportTypes)) {
             throw new IllegalStateException("Must have a single non-test transport specified to "
                     + "use setNetworkSpecifier");
         }
@@ -1786,6 +1830,12 @@
         return this;
     }
 
+    private boolean specifierAcceptableForMultipleTransports(long transportTypes) {
+        return (transportTypes & ~(1L << TRANSPORT_TEST))
+                // Cellular and satellite use the same NetworkSpecifier.
+                == (1 << TRANSPORT_CELLULAR | 1 << TRANSPORT_SATELLITE);
+    }
+
     /**
      * Sets the optional transport specific information.
      *
@@ -2585,6 +2635,7 @@
             case NET_CAPABILITY_PRIORITIZE_LATENCY:          return "PRIORITIZE_LATENCY";
             case NET_CAPABILITY_PRIORITIZE_BANDWIDTH:        return "PRIORITIZE_BANDWIDTH";
             case NET_CAPABILITY_LOCAL_NETWORK:        return "LOCAL_NETWORK";
+            case NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED:    return "NOT_BANDWIDTH_CONSTRAINED";
             default:                                  return Integer.toString(capability);
         }
     }
@@ -2628,12 +2679,6 @@
         return capability >= 0 && capability <= MAX_NET_CAPABILITY;
     }
 
-    private static void checkValidCapability(@NetworkCapabilities.NetCapability int capability) {
-        if (!isValidCapability(capability)) {
-            throw new IllegalArgumentException("NetworkCapability " + capability + " out of range");
-        }
-    }
-
     private static boolean isValidEnterpriseId(
             @NetworkCapabilities.EnterpriseId int enterpriseId) {
         return enterpriseId >= NET_ENTERPRISE_ID_1
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 4de02ac..502ac6f 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -20,7 +20,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -41,8 +41,6 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
-// TODO : replace with android.net.flags.Flags when aconfig is supported on udc-mainline-prod
-// import android.net.NetworkCapabilities.Flags;
 import android.net.NetworkCapabilities.NetCapability;
 import android.net.NetworkCapabilities.Transport;
 import android.os.Build;
@@ -289,19 +287,8 @@
                 NET_CAPABILITY_PARTIAL_CONNECTIVITY,
                 NET_CAPABILITY_TEMPORARILY_NOT_METERED,
                 NET_CAPABILITY_TRUSTED,
-                NET_CAPABILITY_VALIDATED);
-
-        /**
-         * Capabilities that are forbidden by default.
-         * Forbidden capabilities only make sense in NetworkRequest, not for network agents.
-         * Therefore these capabilities are only in NetworkRequest.
-         */
-        private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
-            // TODO(b/313030307): this should contain NET_CAPABILITY_LOCAL_NETWORK.
-            // We cannot currently add it because doing so would crash if the module rolls back,
-            // because JobScheduler persists NetworkRequests to disk, and existing production code
-            // does not consider LOCAL_NETWORK to be a valid capability.
-        };
+                NET_CAPABILITY_VALIDATED,
+                NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
 
         private final NetworkCapabilities mNetworkCapabilities;
 
@@ -318,16 +305,6 @@
             // it for apps that do not have the NETWORK_SETTINGS permission.
             mNetworkCapabilities = new NetworkCapabilities();
             mNetworkCapabilities.setSingleUid(Process.myUid());
-            // Default forbidden capabilities are foremost meant to help with backward
-            // compatibility. When adding new types of network identified by a capability that
-            // might confuse older apps, a default forbidden capability will have apps not see
-            // these networks unless they explicitly ask for it.
-            // If the app called clearCapabilities() it will see everything, but then it
-            // can be argued that it's fair to send them too, since it asked for everything
-            // explicitly.
-            for (final int forbiddenCap : DEFAULT_FORBIDDEN_CAPABILITIES) {
-                mNetworkCapabilities.addForbiddenCapability(forbiddenCap);
-            }
         }
 
         /**
diff --git a/framework/src/android/net/NetworkStackBpfNetMaps.java b/framework/src/android/net/NetworkStackBpfNetMaps.java
new file mode 100644
index 0000000..b7c4e34
--- /dev/null
+++ b/framework/src/android/net/NetworkStackBpfNetMaps.java
@@ -0,0 +1,187 @@
+/*
+ * 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 android.net;
+
+import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
+import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.S32;
+import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
+
+/**
+ * A helper class to *read* java BpfMaps for network stack.
+ * BpfMap operations that are not used from network stack should be in
+ * {@link com.android.server.BpfNetMaps}
+ * @hide
+ */
+// NetworkStack can not use this before U due to b/326143935
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class NetworkStackBpfNetMaps {
+    private static final String TAG = NetworkStackBpfNetMaps.class.getSimpleName();
+
+    // Locally store the handle of bpf maps. The FileDescriptors are statically cached inside the
+    // BpfMap implementation.
+
+    // Bpf map to store various networking configurations, the format of the value is different
+    // for different keys. See BpfNetMapsConstants#*_CONFIGURATION_KEY for keys.
+    private final IBpfMap<S32, U32> mConfigurationMap;
+    // Bpf map to store per uid traffic control configurations.
+    // See {@link UidOwnerValue} for more detail.
+    private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap;
+    private final IBpfMap<S32, U8> mDataSaverEnabledMap;
+    private final Dependencies mDeps;
+
+    private static class SingletonHolder {
+        static final NetworkStackBpfNetMaps sInstance = new NetworkStackBpfNetMaps();
+    }
+
+    @NonNull
+    public static NetworkStackBpfNetMaps getInstance() {
+        return SingletonHolder.sInstance;
+    }
+
+    private NetworkStackBpfNetMaps() {
+        this(new Dependencies());
+    }
+
+    // While the production code uses the singleton to optimize for performance and deal with
+    // concurrent access, the test needs to use a non-static approach for dependency injection and
+    // mocking virtual bpf maps.
+    @VisibleForTesting
+    public NetworkStackBpfNetMaps(@NonNull Dependencies deps) {
+        if (!SdkLevel.isAtLeastT()) {
+            throw new UnsupportedOperationException(
+                    NetworkStackBpfNetMaps.class.getSimpleName()
+                            + " is not supported below Android T");
+        }
+        mDeps = deps;
+        mConfigurationMap = mDeps.getConfigurationMap();
+        mUidOwnerMap = mDeps.getUidOwnerMap();
+        mDataSaverEnabledMap = mDeps.getDataSaverEnabledMap();
+    }
+
+    /**
+     * Dependencies of BpfNetMapReader, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /** Get the configuration map. */
+        public IBpfMap<S32, U32> getConfigurationMap() {
+            try {
+                return new BpfMap<>(CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDONLY,
+                        S32.class, U32.class);
+            } catch (ErrnoException e) {
+                throw new IllegalStateException("Cannot open configuration map", e);
+            }
+        }
+
+        /** Get the uid owner map. */
+        public IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
+            try {
+                return new BpfMap<>(UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDONLY,
+                        S32.class, UidOwnerValue.class);
+            } catch (ErrnoException e) {
+                throw new IllegalStateException("Cannot open uid owner map", e);
+            }
+        }
+
+        /** Get the data saver enabled map. */
+        public  IBpfMap<S32, U8> getDataSaverEnabledMap() {
+            try {
+                return new BpfMap<>(DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDONLY, S32.class,
+                        U8.class);
+            } catch (ErrnoException e) {
+                throw new IllegalStateException("Cannot open data saver enabled map", e);
+            }
+        }
+    }
+
+    /**
+     * Get the specified firewall chain's status.
+     *
+     * @param chain target chain
+     * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public boolean isChainEnabled(final int chain) {
+        return BpfNetMapsUtils.isChainEnabled(mConfigurationMap, chain);
+    }
+
+    /**
+     * Get firewall rule of specified firewall chain on specified uid.
+     *
+     * @param chain target chain
+     * @param uid        target uid
+     * @return either {@link ConnectivityManager#FIREWALL_RULE_ALLOW} or
+     *         {@link ConnectivityManager#FIREWALL_RULE_DENY}.
+     * @throws UnsupportedOperationException if called on pre-T devices.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public int getUidRule(final int chain, final int uid) {
+        return BpfNetMapsUtils.getUidRule(mUidOwnerMap, chain, uid);
+    }
+
+    /**
+     * Return whether the network is blocked by firewall chains for the given uid.
+     *
+     * Note that {@link #getDataSaverEnabled()} has a latency before V.
+     *
+     * @param uid The target uid.
+     * @param isNetworkMetered Whether the target network is metered.
+     *
+     * @return True if the network is blocked. Otherwise, false.
+     * @throws ServiceSpecificException if the read fails.
+     *
+     * @hide
+     */
+    public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered) {
+        return BpfNetMapsUtils.isUidNetworkingBlocked(uid, isNetworkMetered,
+                mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap);
+    }
+
+    /**
+     * Get Data Saver enabled or disabled
+     *
+     * Note that before V, the data saver status in bpf is written by ConnectivityService
+     * when receiving {@link ConnectivityManager#ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+     * the status is not synchronized.
+     * On V+, the data saver status is set by platform code when enabling/disabling
+     * data saver, which is synchronized.
+     *
+     * @return whether Data Saver is enabled or disabled.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public boolean getDataSaverEnabled() {
+        return BpfNetMapsUtils.getDataSaverEnabled(mDataSaverEnabledMap);
+    }
+}
diff --git a/framework/src/android/net/apf/ApfCapabilities.java b/framework/src/android/net/apf/ApfCapabilities.java
index fae2499..f92cdbb 100644
--- a/framework/src/android/net/apf/ApfCapabilities.java
+++ b/framework/src/android/net/apf/ApfCapabilities.java
@@ -22,6 +22,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Objects;
+
 /**
  * APF program support capabilities. APF stands for Android Packet Filtering and it is a flexible
  * way to drop unwanted network packets to save power.
@@ -102,15 +104,22 @@
                 && apfPacketFormat == other.apfPacketFormat;
     }
 
+    @Override
+    public int hashCode() {
+        // hashCode it is not implemented in R. Therefore it would be dangerous for
+        // NetworkStack to depend on it.
+        return Objects.hash(apfVersionSupported, maximumApfProgramSize, apfPacketFormat);
+    }
+
     /**
      * Determines whether the APF interpreter advertises support for the data buffer access opcodes
      * LDDW (LoaD Data Word) and STDW (STore Data Word). Full LDDW (LoaD Data Word) and
-     * STDW (STore Data Word) support is present from APFv4 on.
+     * STDW (STore Data Word) support is present from APFv3 on.
      *
      * @return {@code true} if the IWifiStaIface#readApfPacketFilterData is supported.
      */
     public boolean hasDataAccess() {
-        return apfVersionSupported >= 4;
+        return apfVersionSupported > 2;
     }
 
     /**
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index dfe5867..51df8ab 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -84,6 +84,57 @@
     @ChangeId
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     public static final long ENABLE_PLATFORM_MDNS_BACKEND = 270306772L;
+
+    /**
+     * Apps targeting Android V or higher receive network callbacks from local networks as default
+     *
+     * Apps targeting lower than {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} need
+     * to add {@link android.net.NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK} to the
+     * {@link android.net.NetworkCapabilities} of the {@link android.net.NetworkRequest} to receive
+     * {@link android.net.ConnectivityManager.NetworkCallback} from local networks.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long ENABLE_MATCH_LOCAL_NETWORK = 319212206L;
+
+    /**
+     * On Android {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher releases,
+     * network access from apps targeting Android 36 or higher that do not have the
+     * {@link android.Manifest.permission#INTERNET} permission is considered blocked.
+     * This results in API behaviors change for apps without
+     * {@link android.Manifest.permission#INTERNET} permission.
+     * {@link android.net.NetworkInfo} returned from {@link android.net.ConnectivityManager} APIs
+     * always has {@link android.net.NetworkInfo.DetailedState#BLOCKED}.
+     * {@link android.net.ConnectivityManager#getActiveNetwork()} always returns null.
+     * {@link android.net.ConnectivityManager.NetworkCallback#onBlockedStatusChanged()} is always
+     * called with blocked=true.
+     * <p>
+     * For backwards compatibility, apps running on older releases, or targeting older SDK levels,
+     * network access from apps without {@link android.Manifest.permission#INTERNET} permission is
+     * considered not blocked even though apps cannot access any networks.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public static final long NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION = 333340911L;
+
+    /**
+     * Enable caching for TrafficStats#get* APIs.
+     *
+     * Apps targeting Android V or later or running on Android V or later may take up to several
+     * seconds to see the updated results.
+     * Apps targeting lower android SDKs do not see cached result for backward compatibility,
+     * results of TrafficStats#get* APIs are reflecting network statistics immediately.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE = 74210811L;
+
     private ConnectivityCompatChanges() {
     }
 }
diff --git a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
index 79f1f65..6e87ed3 100644
--- a/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
+++ b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.net.ConnectivityManager;
-import android.net.RoutingCoordinatorManager;
 import android.os.Build;
 import android.os.IBinder;
 
@@ -54,8 +53,8 @@
      * @return an instance of the coordinator manager
      */
     @RequiresApi(Build.VERSION_CODES.S)
-    public static RoutingCoordinatorManager getRoutingCoordinatorManager(Context ctx) {
+    public static IBinder getRoutingCoordinator(Context ctx) {
         final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
-        return cm.getRoutingCoordinatorManager();
+        return cm.getRoutingCoordinatorService();
     }
 }
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 4be102c..41a28a0 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -58,4 +58,7 @@
     visibility: [
         "//packages/modules/Connectivity/nearby/tests:__subpackages__",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/nearby/framework/lint-baseline.xml b/nearby/framework/lint-baseline.xml
new file mode 100644
index 0000000..e1081ee
--- /dev/null
+++ b/nearby/framework/lint-baseline.xml
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="87"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="87"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="87"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="95"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="95"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="95"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="95"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="103"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="529"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="573"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="    @FlaggedApi(&quot;com.android.nearby.flags.powered_off_finding&quot;)"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/framework/java/android/nearby/NearbyManager.java"
+            line="605"
+            column="17"/>
+    </issue>
+
+</issues>
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 832ac03..1e36676 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -28,9 +28,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import android.app.UiAutomation;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.cts.BTAdapterUtils;
+import android.bluetooth.test_utils.EnableBluetoothRule;
 import android.content.Context;
 import android.location.LocationManager;
 import android.nearby.BroadcastCallback;
@@ -58,6 +56,7 @@
 import com.android.modules.utils.build.SdkLevel;
 
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -76,6 +75,9 @@
 @RunWith(AndroidJUnit4.class)
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class NearbyManagerTest {
+
+    @ClassRule public static final EnableBluetoothRule sEnableBluetooth = new EnableBluetoothRule();
+
     private static final byte[] SALT = new byte[]{1, 2};
     private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
     private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
@@ -128,8 +130,6 @@
 
         mContext = InstrumentationRegistry.getContext();
         mNearbyManager = mContext.getSystemService(NearbyManager.class);
-
-        enableBluetooth();
     }
 
     @Test
@@ -281,14 +281,6 @@
         assertThrows(SecurityException.class, () -> mNearbyManager.getPoweredOffFindingMode());
     }
 
-    private void enableBluetooth() {
-        BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
-        BluetoothAdapter bluetoothAdapter = manager.getAdapter();
-        if (!bluetoothAdapter.isEnabled()) {
-            assertThat(BTAdapterUtils.enableAdapter(bluetoothAdapter, mContext)).isTrue();
-        }
-    }
-
     private void enableLocation() {
         LocationManager locationManager = mContext.getSystemService(LocationManager.class);
         UserHandle user = Process.myUserHandle();
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
index 671b5c5..32286e1 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
@@ -33,6 +34,8 @@
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -68,6 +71,8 @@
 
     @Before
     public void setup() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
         MockitoAnnotations.initMocks(this);
         mBluetoothFinderManager = new BluetoothFinderManagerSpy();
     }
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index b71890e..b8c0ce7 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -18,6 +18,18 @@
     default_team: "trendy_team_fwk_core_networking",
 }
 
+install_symlink {
+    name: "mainline_tethering_platform_components",
+
+    symlink_target: "/apex/com.android.tethering/bin/ethtool",
+    // installed_location is relative to /system because that's the default partition for soong
+    // modules, unless we add something like `system_ext_specific: true` like in hwservicemanager.
+    installed_location: "bin/ethtool",
+
+    init_rc: ["netbpfload.rc"],
+    required: ["bpfloader"],
+}
+
 cc_binary {
     name: "netbpfload",
 
@@ -36,10 +48,7 @@
         "libbase",
         "liblog",
     ],
-    srcs: [
-        "loader.cpp",
-        "NetBpfLoad.cpp",
-    ],
+    srcs: ["NetBpfLoad.cpp"],
     apex_available: [
         "com.android.tethering",
         "//apex_available:platform",
@@ -49,9 +58,7 @@
     // module "netbpfload" variant "android_x86_apex30": should support
     // min_sdk_version(30) for "com.android.tethering": newer SDK(34).
     min_sdk_version: "30",
-
-    init_rc: ["netbpfload.rc"],
-    required: ["bpfloader"],
+    installable: false,
 }
 
 // Versioned netbpfload init rc: init system will process it only on api T/33+ devices
@@ -60,8 +67,15 @@
 // For details of versioned rc files see:
 // https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
 prebuilt_etc {
-    name: "netbpfload.mainline.rc",
-    src: "netbpfload.mainline.rc",
+    name: "netbpfload.33rc",
+    src: "netbpfload.33rc",
     filename: "netbpfload.33rc",
     installable: false,
 }
+
+prebuilt_etc {
+    name: "netbpfload.35rc",
+    src: "netbpfload.35rc",
+    filename: "netbpfload.35rc",
+    installable: false,
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index ed7d048..afb44cc 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017-2023 The Android Open Source Project
+ * Copyright (C) 2018-2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,60 +14,1229 @@
  * limitations under the License.
  */
 
-#ifndef LOG_TAG
 #define LOG_TAG "NetBpfLoad"
-#endif
 
 #include <arpa/inet.h>
+#include <cstdlib>
 #include <dirent.h>
 #include <elf.h>
+#include <errno.h>
 #include <error.h>
 #include <fcntl.h>
+#include <fstream>
 #include <inttypes.h>
+#include <iostream>
 #include <linux/bpf.h>
+#include <linux/elf.h>
 #include <linux/unistd.h>
+#include <log/log.h>
 #include <net/if.h>
+#include <optional>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <unistd.h>
-
+#include <string>
 #include <sys/mman.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <sysexits.h>
+#include <unistd.h>
+#include <unordered_map>
+#include <vector>
 
-#include <android/api-level.h>
+#include <android-base/cmsg.h>
+#include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/macros.h>
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 #include <android-base/unique_fd.h>
-#include <log/log.h>
+#include <android/api-level.h>
 
 #include "BpfSyscallWrappers.h"
 #include "bpf/BpfUtils.h"
-#include "loader.h"
+#include "bpf/bpf_map_def.h"
 
 using android::base::EndsWith;
-using android::bpf::domain;
+using android::base::StartsWith;
+using android::base::unique_fd;
+using std::ifstream;
+using std::ios;
+using std::optional;
 using std::string;
+using std::vector;
 
-bool exists(const char* const path) {
-    int v = access(path, F_OK);
-    if (!v) {
-        ALOGI("%s exists.", path);
+namespace android {
+namespace bpf {
+
+// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
+//
+// The BpfLoader needs to convert these bpf.o specified strings into an enum
+// for internal use (to check that valid values were specified for the specific
+// location of the bpf.o file).
+//
+// It also needs to map selinux_context's into pin_subdir's.
+// This is because of how selinux_context is actually implemented via pin+rename.
+//
+// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
+// is aware of.  Thus there currently needs to be a 1:1 mapping between the two.
+//
+enum class domain : int {
+    unspecified = 0,    // means just use the default for that specific pin location
+    tethering,          // (S+) fs_bpf_tethering     /sys/fs/bpf/tethering
+    net_private,        // (T+) fs_bpf_net_private   /sys/fs/bpf/net_private
+    net_shared,         // (T+) fs_bpf_net_shared    /sys/fs/bpf/net_shared
+    netd_readonly,      // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
+    netd_shared,        // (T+) fs_bpf_netd_shared   /sys/fs/bpf/netd_shared
+};
+
+static constexpr domain AllDomains[] = {
+    domain::unspecified,
+    domain::tethering,
+    domain::net_private,
+    domain::net_shared,
+    domain::netd_readonly,
+    domain::netd_shared,
+};
+
+static constexpr bool specified(domain d) {
+    return d != domain::unspecified;
+}
+
+struct Location {
+    const char* const dir = "";
+    const char* const prefix = "";
+};
+
+// Returns the build type string (from ro.build.type).
+const std::string& getBuildType() {
+    static std::string t = android::base::GetProperty("ro.build.type", "unknown");
+    return t;
+}
+
+// The following functions classify the 3 Android build types.
+inline bool isEng() {
+    return getBuildType() == "eng";
+}
+
+inline bool isUser() {
+    return getBuildType() == "user";
+}
+
+inline bool isUserdebug() {
+    return getBuildType() == "userdebug";
+}
+
+#define BPF_FS_PATH "/sys/fs/bpf/"
+
+// Size of the BPF log buffer for verifier logging
+#define BPF_LOAD_LOG_SZ 0xfffff
+
+static unsigned int page_size = static_cast<unsigned int>(getpagesize());
+
+constexpr const char* lookupSelinuxContext(const domain d) {
+    switch (d) {
+        case domain::unspecified:   return "";
+        case domain::tethering:     return "fs_bpf_tethering";
+        case domain::net_private:   return "fs_bpf_net_private";
+        case domain::net_shared:    return "fs_bpf_net_shared";
+        case domain::netd_readonly: return "fs_bpf_netd_readonly";
+        case domain::netd_shared:   return "fs_bpf_netd_shared";
+    }
+}
+
+domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
+    for (domain d : AllDomains) {
+        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+        if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
+        if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
+    }
+    ALOGE("unrecognized selinux_context '%-32s'", s);
+    // Note: we *can* just abort() here as we only load bpf .o files shipped
+    // in the same mainline module / apex as NetBpfLoad itself.
+    abort();
+}
+
+constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
+    switch (d) {
+        case domain::unspecified:   return unspecified;
+        case domain::tethering:     return "tethering/";
+        case domain::net_private:   return "net_private/";
+        case domain::net_shared:    return "net_shared/";
+        case domain::netd_readonly: return "netd_readonly/";
+        case domain::netd_shared:   return "netd_shared/";
+    }
+};
+
+domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
+    for (domain d : AllDomains) {
+        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+        if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
+        if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
+    }
+    ALOGE("unrecognized pin_subdir '%-32s'", s);
+    // Note: we *can* just abort() here as we only load bpf .o files shipped
+    // in the same mainline module / apex as NetBpfLoad itself.
+    abort();
+}
+
+static string pathToObjName(const string& path) {
+    // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
+    string filename = android::base::Split(path, "/").back();
+    // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
+    string name = filename.substr(0, filename.find_last_of('.'));
+    // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
+    // this can be used to provide duplicate programs (mux based on the bpfloader version)
+    return name.substr(0, name.find_last_of('@'));
+}
+
+typedef struct {
+    const char* name;
+    enum bpf_prog_type type;
+    enum bpf_attach_type attach_type;
+} sectionType;
+
+/*
+ * Map section name prefixes to program types, the section name will be:
+ *   SECTION(<prefix>/<name-of-program>)
+ * For example:
+ *   SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
+ * is the name of the program, and tracepoint is the type.
+ *
+ * However, be aware that you should not be directly using the SECTION() macro.
+ * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
+ *
+ * Programs shipped inside the tethering apex should be limited to networking stuff,
+ * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
+ * since they are less stable abi/api and may conflict with platform uses of bpf.
+ */
+sectionType sectionNameTypes[] = {
+        {"bind4/",             BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
+        {"bind6/",             BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
+        {"cgroupskb/",         BPF_PROG_TYPE_CGROUP_SKB},
+        {"cgroupsock/",        BPF_PROG_TYPE_CGROUP_SOCK},
+        {"cgroupsockcreate/",  BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_CREATE},
+        {"cgroupsockrelease/", BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET_SOCK_RELEASE},
+        {"connect4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
+        {"connect6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
+        {"egress/",            BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_EGRESS},
+        {"getsockopt/",        BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_GETSOCKOPT},
+        {"ingress/",           BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_INGRESS},
+        {"postbind4/",         BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET4_POST_BIND},
+        {"postbind6/",         BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET6_POST_BIND},
+        {"recvmsg4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
+        {"recvmsg6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
+        {"schedact/",          BPF_PROG_TYPE_SCHED_ACT},
+        {"schedcls/",          BPF_PROG_TYPE_SCHED_CLS},
+        {"sendmsg4/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
+        {"sendmsg6/",          BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
+        {"setsockopt/",        BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_SETSOCKOPT},
+        {"skfilter/",          BPF_PROG_TYPE_SOCKET_FILTER},
+        {"sockops/",           BPF_PROG_TYPE_SOCK_OPS,         BPF_CGROUP_SOCK_OPS},
+        {"sysctl",             BPF_PROG_TYPE_CGROUP_SYSCTL,    BPF_CGROUP_SYSCTL},
+        {"xdp/",               BPF_PROG_TYPE_XDP},
+};
+
+typedef struct {
+    enum bpf_prog_type type;
+    enum bpf_attach_type attach_type;
+    string name;
+    vector<char> data;
+    vector<char> rel_data;
+    optional<struct bpf_prog_def> prog_def;
+
+    unique_fd prog_fd; /* fd after loading */
+} codeSection;
+
+static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
+    elfFile.seekg(0);
+    if (elfFile.fail()) return -1;
+
+    if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
+
+    return 0;
+}
+
+/* Reads all section header tables into an Shdr array */
+static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
+    Elf64_Ehdr eh;
+    int ret = 0;
+
+    ret = readElfHeader(elfFile, &eh);
+    if (ret) return ret;
+
+    elfFile.seekg(eh.e_shoff);
+    if (elfFile.fail()) return -1;
+
+    /* Read shdr table entries */
+    shTable.resize(eh.e_shnum);
+
+    if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
+
+    return 0;
+}
+
+/* Read a section by its index - for ex to get sec hdr strtab blob */
+static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
+    vector<Elf64_Shdr> shTable;
+    int ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    elfFile.seekg(shTable[id].sh_offset);
+    if (elfFile.fail()) return -1;
+
+    sec.resize(shTable[id].sh_size);
+    if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
+
+    return 0;
+}
+
+/* Read whole section header string table */
+static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
+    Elf64_Ehdr eh;
+    int ret = readElfHeader(elfFile, &eh);
+    if (ret) return ret;
+
+    ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
+    if (ret) return ret;
+
+    return 0;
+}
+
+/* Get name from offset in strtab */
+static int getSymName(ifstream& elfFile, int nameOff, string& name) {
+    int ret;
+    vector<char> secStrTab;
+
+    ret = readSectionHeaderStrtab(elfFile, secStrTab);
+    if (ret) return ret;
+
+    if (nameOff >= (int)secStrTab.size()) return -1;
+
+    name = string((char*)secStrTab.data() + nameOff);
+    return 0;
+}
+
+/* Reads a full section by name - example to get the GPL license */
+static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
+    vector<char> secStrTab;
+    vector<Elf64_Shdr> shTable;
+    int ret;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    ret = readSectionHeaderStrtab(elfFile, secStrTab);
+    if (ret) return ret;
+
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        char* secname = secStrTab.data() + shTable[i].sh_name;
+        if (!secname) continue;
+
+        if (!strcmp(secname, name)) {
+            vector<char> dataTmp;
+            dataTmp.resize(shTable[i].sh_size);
+
+            elfFile.seekg(shTable[i].sh_offset);
+            if (elfFile.fail()) return -1;
+
+            if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+            data = dataTmp;
+            return 0;
+        }
+    }
+    return -2;
+}
+
+unsigned int readSectionUint(const char* name, ifstream& elfFile, unsigned int defVal) {
+    vector<char> theBytes;
+    int ret = readSectionByName(name, elfFile, theBytes);
+    if (ret) {
+        ALOGD("Couldn't find section %s (defaulting to %u [0x%x]).", name, defVal, defVal);
+        return defVal;
+    } else if (theBytes.size() < sizeof(unsigned int)) {
+        ALOGE("Section %s too short (defaulting to %u [0x%x]).", name, defVal, defVal);
+        return defVal;
+    } else {
+        // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
+        unsigned int value = static_cast<unsigned char>(theBytes[3]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[2]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[1]);
+        value <<= 8;
+        value += static_cast<unsigned char>(theBytes[0]);
+        ALOGI("Section %s value is %u [0x%x]", name, value, value);
+        return value;
+    }
+}
+
+static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
+    int ret;
+    vector<Elf64_Shdr> shTable;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        if ((int)shTable[i].sh_type != type) continue;
+
+        vector<char> dataTmp;
+        dataTmp.resize(shTable[i].sh_size);
+
+        elfFile.seekg(shTable[i].sh_offset);
+        if (elfFile.fail()) return -1;
+
+        if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+        data = dataTmp;
+        return 0;
+    }
+    return -2;
+}
+
+static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
+    return (a.st_value < b.st_value);
+}
+
+static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
+    int ret, numElems;
+    Elf64_Sym* buf;
+    vector<char> secData;
+
+    ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
+    if (ret) return ret;
+
+    buf = (Elf64_Sym*)secData.data();
+    numElems = (secData.size() / sizeof(Elf64_Sym));
+    data.assign(buf, buf + numElems);
+
+    if (sort) std::sort(data.begin(), data.end(), symCompare);
+    return 0;
+}
+
+static enum bpf_prog_type getSectionType(string& name) {
+    for (auto& snt : sectionNameTypes)
+        if (StartsWith(name, snt.name)) return snt.type;
+
+    return BPF_PROG_TYPE_UNSPEC;
+}
+
+/*
+static string getSectionName(enum bpf_prog_type type)
+{
+    for (auto& snt : sectionNameTypes)
+        if (snt.type == type)
+            return string(snt.name);
+
+    return "UNKNOWN SECTION NAME " + std::to_string(type);
+}
+*/
+
+static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
+                        size_t sizeOfBpfProgDef) {
+    vector<char> pdData;
+    int ret = readSectionByName("progs", elfFile, pdData);
+    if (ret) return ret;
+
+    if (pdData.size() % sizeOfBpfProgDef) {
+        ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
+              pdData.size(), sizeOfBpfProgDef);
+        return -1;
+    };
+
+    int progCount = pdData.size() / sizeOfBpfProgDef;
+    pd.resize(progCount);
+    size_t trimmedSize = std::min(sizeOfBpfProgDef, sizeof(struct bpf_prog_def));
+
+    const char* dataPtr = pdData.data();
+    for (auto& p : pd) {
+        // First we zero initialize
+        memset(&p, 0, sizeof(p));
+        // Then we set non-zero defaults
+        p.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
+        // Then we copy over the structure prefix from the ELF file.
+        memcpy(&p, dataPtr, trimmedSize);
+        // Move to next struct in the ELF file
+        dataPtr += sizeOfBpfProgDef;
+    }
+    return 0;
+}
+
+static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
+                              optional<unsigned> symbolType = std::nullopt) {
+    int ret;
+    string name;
+    vector<Elf64_Sym> symtab;
+    vector<Elf64_Shdr> shTable;
+
+    ret = readSymTab(elfFile, 1 /* sort */, symtab);
+    if (ret) return ret;
+
+    /* Get index of section */
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+
+    int sec_idx = -1;
+    for (int i = 0; i < (int)shTable.size(); i++) {
+        ret = getSymName(elfFile, shTable[i].sh_name, name);
+        if (ret) return ret;
+
+        if (!name.compare(sectionName)) {
+            sec_idx = i;
+            break;
+        }
+    }
+
+    /* No section found with matching name*/
+    if (sec_idx == -1) {
+        ALOGW("No %s section could be found in elf object", sectionName.c_str());
+        return -1;
+    }
+
+    for (int i = 0; i < (int)symtab.size(); i++) {
+        if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
+
+        if (symtab[i].st_shndx == sec_idx) {
+            string s;
+            ret = getSymName(elfFile, symtab[i].st_name, s);
+            if (ret) return ret;
+            names.push_back(s);
+        }
+    }
+
+    return 0;
+}
+
+/* Read a section by its index - for ex to get sec hdr strtab blob */
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
+    vector<Elf64_Shdr> shTable;
+    int entries, ret = 0;
+
+    ret = readSectionHeadersAll(elfFile, shTable);
+    if (ret) return ret;
+    entries = shTable.size();
+
+    vector<struct bpf_prog_def> pd;
+    ret = readProgDefs(elfFile, pd, sizeOfBpfProgDef);
+    if (ret) return ret;
+    vector<string> progDefNames;
+    ret = getSectionSymNames(elfFile, "progs", progDefNames);
+    if (!pd.empty() && ret) return ret;
+
+    for (int i = 0; i < entries; i++) {
+        string name;
+        codeSection cs_temp;
+        cs_temp.type = BPF_PROG_TYPE_UNSPEC;
+
+        ret = getSymName(elfFile, shTable[i].sh_name, name);
+        if (ret) return ret;
+
+        enum bpf_prog_type ptype = getSectionType(name);
+
+        if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
+
+        // This must be done before '/' is replaced with '_'.
+        for (auto& snt : sectionNameTypes)
+            if (StartsWith(name, snt.name)) cs_temp.attach_type = snt.attach_type;
+
+        string oldName = name;
+
+        // convert all slashes to underscores
+        std::replace(name.begin(), name.end(), '/', '_');
+
+        cs_temp.type = ptype;
+        cs_temp.name = name;
+
+        ret = readSectionByIdx(elfFile, i, cs_temp.data);
+        if (ret) return ret;
+        ALOGV("Loaded code section %d (%s)", i, name.c_str());
+
+        vector<string> csSymNames;
+        ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
+        if (ret || !csSymNames.size()) return ret;
+        for (size_t i = 0; i < progDefNames.size(); ++i) {
+            if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
+                cs_temp.prog_def = pd[i];
+                break;
+            }
+        }
+
+        /* Check for rel section */
+        if (cs_temp.data.size() > 0 && i < entries) {
+            ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
+            if (ret) return ret;
+
+            if (name == (".rel" + oldName)) {
+                ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
+                if (ret) return ret;
+                ALOGV("Loaded relo section %d (%s)", i, name.c_str());
+            }
+        }
+
+        if (cs_temp.data.size() > 0) {
+            cs.push_back(std::move(cs_temp));
+            ALOGV("Adding section %d to cs list", i);
+        }
+    }
+    return 0;
+}
+
+static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
+    vector<Elf64_Sym> symtab;
+    int ret = 0;
+
+    ret = readSymTab(elfFile, 0 /* !sort */, symtab);
+    if (ret) return ret;
+
+    if (index >= (int)symtab.size()) return -1;
+
+    return getSymName(elfFile, symtab[index].st_name, name);
+}
+
+static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
+                                   const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
+    // bpfGetFd... family of functions require at minimum a 4.14 kernel,
+    // so on 4.9-T kernels just pretend the map matches our expectations.
+    // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
+    // This is because the primary failure mode we're trying to detect here
+    // is either a source code misconfiguration (which is likely kernel independent)
+    // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
+    if (!isAtLeastKernelVersion(4, 14, 0)) return true;
+
+    // Assuming fd is a valid Bpf Map file descriptor then
+    // all the following should always succeed on a 4.14+ kernel.
+    // If they somehow do fail, they'll return -1 (and set errno),
+    // which should then cause (among others) a key_size mismatch.
+    int fd_type = bpfGetFdMapType(fd);
+    int fd_key_size = bpfGetFdKeySize(fd);
+    int fd_value_size = bpfGetFdValueSize(fd);
+    int fd_max_entries = bpfGetFdMaxEntries(fd);
+    int fd_map_flags = bpfGetFdMapFlags(fd);
+
+    // DEVMAPs are readonly from the bpf program side's point of view, as such
+    // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
+    int desired_map_flags = (int)mapDef.map_flags;
+    if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
+        desired_map_flags |= BPF_F_RDONLY_PROG;
+
+    // The .h file enforces that this is a power of two, and page size will
+    // also always be a power of two, so this logic is actually enough to
+    // force it to be a multiple of the page size, as required by the kernel.
+    unsigned int desired_max_entries = mapDef.max_entries;
+    if (type == BPF_MAP_TYPE_RINGBUF) {
+        if (desired_max_entries < page_size) desired_max_entries = page_size;
+    }
+
+    // The following checks should *never* trigger, if one of them somehow does,
+    // it probably means a bpf .o file has been changed/replaced at runtime
+    // and bpfloader was manually rerun (normally it should only run *once*
+    // early during the boot process).
+    // Another possibility is that something is misconfigured in the code:
+    // most likely a shared map is declared twice differently.
+    // But such a change should never be checked into the source tree...
+    if ((fd_type == type) &&
+        (fd_key_size == (int)mapDef.key_size) &&
+        (fd_value_size == (int)mapDef.value_size) &&
+        (fd_max_entries == (int)desired_max_entries) &&
+        (fd_map_flags == desired_map_flags)) {
         return true;
     }
+
+    ALOGE("bpf map name %s mismatch: desired/found: "
+          "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
+          mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
+          fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
+    return false;
+}
+
+static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
+                      const char* prefix, const size_t sizeOfBpfMapDef,
+                      const unsigned int bpfloader_ver) {
+    int ret;
+    vector<char> mdData;
+    vector<struct bpf_map_def> md;
+    vector<string> mapNames;
+    string objName = pathToObjName(string(elfPath));
+
+    ret = readSectionByName("maps", elfFile, mdData);
+    if (ret == -2) return 0;  // no maps to read
+    if (ret) return ret;
+
+    if (mdData.size() % sizeOfBpfMapDef) {
+        ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
+              mdData.size(), sizeOfBpfMapDef);
+        return -1;
+    };
+
+    int mapCount = mdData.size() / sizeOfBpfMapDef;
+    md.resize(mapCount);
+    size_t trimmedSize = std::min(sizeOfBpfMapDef, sizeof(struct bpf_map_def));
+
+    const char* dataPtr = mdData.data();
+    for (auto& m : md) {
+        // First we zero initialize
+        memset(&m, 0, sizeof(m));
+        // Then we set non-zero defaults
+        m.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
+        m.max_kver = 0xFFFFFFFFu;                         // matches KVER_INF from bpf_helpers.h
+        // Then we copy over the structure prefix from the ELF file.
+        memcpy(&m, dataPtr, trimmedSize);
+        // Move to next struct in the ELF file
+        dataPtr += sizeOfBpfMapDef;
+    }
+
+    ret = getSectionSymNames(elfFile, "maps", mapNames);
+    if (ret) return ret;
+
+    unsigned kvers = kernelVersion();
+
+    for (int i = 0; i < (int)mapNames.size(); i++) {
+        if (md[i].zero != 0) abort();
+
+        if (bpfloader_ver < md[i].bpfloader_min_ver) {
+            ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
+                  md[i].bpfloader_min_ver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (bpfloader_ver >= md[i].bpfloader_max_ver) {
+            ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
+                  md[i].bpfloader_max_ver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (kvers < md[i].min_kver) {
+            ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
+                  mapNames[i].c_str(), kvers, md[i].min_kver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if (kvers >= md[i].max_kver) {
+            ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
+                  mapNames[i].c_str(), kvers, md[i].max_kver);
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
+            (md[i].ignore_on_userdebug && isUserdebug())) {
+            ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
+                  getBuildType().c_str());
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
+            (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
+            (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
+            (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
+            (isRiscV() && md[i].ignore_on_riscv64)) {
+            ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
+                  describeArch());
+            mapFds.push_back(unique_fd());
+            continue;
+        }
+
+        enum bpf_map_type type = md[i].type;
+        if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
+            // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
+            // of be approximated: ARRAY has the same userspace api, though it is not usable
+            // by the same ebpf programs.  However, that's okay because the bpf_redirect_map()
+            // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
+            // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
+            // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
+            // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
+            type = BPF_MAP_TYPE_ARRAY;
+        }
+        if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
+            // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
+            // of be approximated: HASH has the same userspace visible api.
+            // However it cannot be used by ebpf programs in the same way.
+            // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
+            // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
+            // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
+            // programs as being 5.4+...
+            type = BPF_MAP_TYPE_HASH;
+        }
+
+        // The .h file enforces that this is a power of two, and page size will
+        // also always be a power of two, so this logic is actually enough to
+        // force it to be a multiple of the page size, as required by the kernel.
+        unsigned int max_entries = md[i].max_entries;
+        if (type == BPF_MAP_TYPE_RINGBUF) {
+            if (max_entries < page_size) max_entries = page_size;
+        }
+
+        domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
+        if (specified(selinux_context)) {
+            ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
+                  md[i].selinux_context, static_cast<int>(selinux_context),
+                  lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+        }
+
+        domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
+        if (specified(pin_subdir)) {
+            ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
+                  static_cast<int>(pin_subdir), lookupPinSubdir(pin_subdir));
+        }
+
+        // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
+        // except that maps shared across .o's have empty <objName>
+        // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
+        string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
+                           (md[i].shared ? "" : objName) + "_" + mapNames[i];
+        bool reuse = false;
+        unique_fd fd;
+        int saved_errno;
+
+        if (access(mapPinLoc.c_str(), F_OK) == 0) {
+            fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
+            saved_errno = errno;
+            ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
+            reuse = true;
+        } else {
+            union bpf_attr req = {
+              .map_type = type,
+              .key_size = md[i].key_size,
+              .value_size = md[i].value_size,
+              .max_entries = max_entries,
+              .map_flags = md[i].map_flags,
+            };
+            if (isAtLeastKernelVersion(4, 15, 0))
+                strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
+            fd.reset(bpf(BPF_MAP_CREATE, req));
+            saved_errno = errno;
+            ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
+        }
+
+        if (!fd.ok()) return -saved_errno;
+
+        // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
+        // safety (since reuse code path is rare) run these checks even if we just created it.
+        // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
+        if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
+
+        if (!reuse) {
+            if (specified(selinux_context)) {
+                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+                                   "tmp_map_" + objName + "_" + mapNames[i];
+                ret = bpfFdPin(fd, createLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+                ret = renameat2(AT_FDCWD, createLoc.c_str(),
+                                AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
+                if (ret) {
+                    int err = errno;
+                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
+                          err, strerror(err));
+                    return -err;
+                }
+            } else {
+                ret = bpfFdPin(fd, mapPinLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+            }
+            ret = chmod(mapPinLoc.c_str(), md[i].mode);
+            if (ret) {
+                int err = errno;
+                ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
+                      strerror(err));
+                return -err;
+            }
+            ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
+            if (ret) {
+                int err = errno;
+                ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
+                      ret, err, strerror(err));
+                return -err;
+            }
+        }
+
+        int mapId = bpfGetFdMapId(fd);
+        if (mapId == -1) {
+            ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
+        } else {
+            ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
+        }
+
+        mapFds.push_back(std::move(fd));
+    }
+
+    return ret;
+}
+
+/* For debugging, dump all instructions */
+static void dumpIns(char* ins, int size) {
+    for (int row = 0; row < size / 8; row++) {
+        ALOGE("%d: ", row);
+        for (int j = 0; j < 8; j++) {
+            ALOGE("%3x ", ins[(row * 8) + j]);
+        }
+        ALOGE("\n");
+    }
+}
+
+/* For debugging, dump all code sections from cs list */
+static void dumpAllCs(vector<codeSection>& cs) {
+    for (int i = 0; i < (int)cs.size(); i++) {
+        ALOGE("Dumping cs %d, name %s", int(i), cs[i].name.c_str());
+        dumpIns((char*)cs[i].data.data(), cs[i].data.size());
+        ALOGE("-----------");
+    }
+}
+
+static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
+    int insnIndex;
+    struct bpf_insn *insn, *insns;
+
+    insns = (struct bpf_insn*)(insnsPtr);
+
+    insnIndex = offset / sizeof(struct bpf_insn);
+    insn = &insns[insnIndex];
+
+    // Occasionally might be useful for relocation debugging, but pretty spammy
+    if (0) {
+        ALOGV("applying relo to instruction at byte offset: %llu, "
+              "insn offset %d, insn %llx",
+              (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
+    }
+
+    if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
+        ALOGE("Dumping all instructions till ins %d", insnIndex);
+        ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
+        dumpIns((char*)insnsPtr, (insnIndex + 3) * 8);
+        return;
+    }
+
+    insn->imm = fd;
+    insn->src_reg = BPF_PSEUDO_MAP_FD;
+}
+
+static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
+    vector<string> mapNames;
+
+    int ret = getSectionSymNames(elfFile, "maps", mapNames);
+    if (ret) return;
+
+    for (int k = 0; k != (int)cs.size(); k++) {
+        Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
+        int n_rel = cs[k].rel_data.size() / sizeof(*rel);
+
+        for (int i = 0; i < n_rel; i++) {
+            int symIndex = ELF64_R_SYM(rel[i].r_info);
+            string symName;
+
+            ret = getSymNameByIdx(elfFile, symIndex, symName);
+            if (ret) return;
+
+            /* Find the map fd and apply relo */
+            for (int j = 0; j < (int)mapNames.size(); j++) {
+                if (!mapNames[j].compare(symName)) {
+                    applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
+                    break;
+                }
+            }
+        }
+    }
+}
+
+static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
+                            const char* prefix, const unsigned int bpfloader_ver) {
+    unsigned kvers = kernelVersion();
+
+    if (!kvers) {
+        ALOGE("unable to get kernel version");
+        return -EINVAL;
+    }
+
+    string objName = pathToObjName(string(elfPath));
+
+    for (int i = 0; i < (int)cs.size(); i++) {
+        unique_fd& fd = cs[i].prog_fd;
+        int ret;
+        string name = cs[i].name;
+
+        if (!cs[i].prog_def.has_value()) {
+            ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
+            return -EINVAL;
+        }
+
+        unsigned min_kver = cs[i].prog_def->min_kver;
+        unsigned max_kver = cs[i].prog_def->max_kver;
+        ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
+             max_kver, kvers);
+        if (kvers < min_kver) continue;
+        if (kvers >= max_kver) continue;
+
+        unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
+        unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
+        domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
+        domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
+
+        ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
+              bpfMinVer, bpfMaxVer);
+        if (bpfloader_ver < bpfMinVer) continue;
+        if (bpfloader_ver >= bpfMaxVer) continue;
+
+        if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
+            (cs[i].prog_def->ignore_on_user && isUser()) ||
+            (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
+            ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
+                  getBuildType().c_str());
+            continue;
+        }
+
+        if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
+            (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
+            (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
+            (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
+            (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
+            ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
+            continue;
+        }
+
+        if (specified(selinux_context)) {
+            ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
+                  cs[i].prog_def->selinux_context, static_cast<int>(selinux_context),
+                  lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+        }
+
+        if (specified(pin_subdir)) {
+            ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
+                  cs[i].prog_def->pin_subdir, static_cast<int>(pin_subdir),
+                  lookupPinSubdir(pin_subdir));
+        }
+
+        // strip any potential $foo suffix
+        // this can be used to provide duplicate programs
+        // conditionally loaded based on running kernel version
+        name = name.substr(0, name.find_last_of('$'));
+
+        bool reuse = false;
+        // Format of pin location is
+        // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
+        string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
+                            objName + '_' + string(name);
+        if (access(progPinLoc.c_str(), F_OK) == 0) {
+            fd.reset(retrieveProgram(progPinLoc.c_str()));
+            ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
+                  (!fd.ok() ? std::strerror(errno) : "no error"));
+            reuse = true;
+        } else {
+            vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
+
+            union bpf_attr req = {
+              .prog_type = cs[i].type,
+              .kern_version = kvers,
+              .license = ptr_to_u64(license.c_str()),
+              .insns = ptr_to_u64(cs[i].data.data()),
+              .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
+              .log_level = 1,
+              .log_buf = ptr_to_u64(log_buf.data()),
+              .log_size = static_cast<__u32>(log_buf.size()),
+              .expected_attach_type = cs[i].attach_type,
+            };
+            if (isAtLeastKernelVersion(4, 15, 0))
+                strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
+            fd.reset(bpf(BPF_PROG_LOAD, req));
+
+            ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
+                  cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
+
+            if (!fd.ok()) {
+                vector<string> lines = android::base::Split(log_buf.data(), "\n");
+
+                ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
+                for (const auto& line : lines) ALOGW("%s", line.c_str());
+                ALOGW("BPF_PROG_LOAD - END log_buf contents.");
+
+                if (cs[i].prog_def->optional) {
+                    ALOGW("failed program is marked optional - continuing...");
+                    continue;
+                }
+                ALOGE("non-optional program failed to load.");
+            }
+        }
+
+        if (!fd.ok()) return fd.get();
+
+        if (!reuse) {
+            if (specified(selinux_context)) {
+                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+                                   "tmp_prog_" + objName + '_' + string(name);
+                ret = bpfFdPin(fd, createLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+                ret = renameat2(AT_FDCWD, createLoc.c_str(),
+                                AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
+                if (ret) {
+                    int err = errno;
+                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
+                          err, strerror(err));
+                    return -err;
+                }
+            } else {
+                ret = bpfFdPin(fd, progPinLoc.c_str());
+                if (ret) {
+                    int err = errno;
+                    ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
+                    return -err;
+                }
+            }
+            if (chmod(progPinLoc.c_str(), 0440)) {
+                int err = errno;
+                ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
+                return -err;
+            }
+            if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
+                      (gid_t)cs[i].prog_def->gid)) {
+                int err = errno;
+                ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
+                      cs[i].prog_def->gid, err, strerror(err));
+                return -err;
+            }
+        }
+
+        int progId = bpfGetFdProgId(fd);
+        if (progId == -1) {
+            ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
+        } else {
+            ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+        }
+    }
+
+    return 0;
+}
+
+int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
+             const Location& location) {
+    vector<char> license;
+    vector<char> critical;
+    vector<codeSection> cs;
+    vector<unique_fd> mapFds;
+    int ret;
+
+    if (!isCritical) return -1;
+    *isCritical = false;
+
+    ifstream elfFile(elfPath, ios::in | ios::binary);
+    if (!elfFile.is_open()) return -1;
+
+    ret = readSectionByName("critical", elfFile, critical);
+    *isCritical = !ret;
+
+    ret = readSectionByName("license", elfFile, license);
+    if (ret) {
+        ALOGE("Couldn't find license in %s", elfPath);
+        return ret;
+    } else {
+        ALOGD("Loading %s%s ELF object %s with license %s",
+              *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
+              elfPath, (char*)license.data());
+    }
+
+    // the following default values are for bpfloader V0.0 format which does not include them
+    unsigned int bpfLoaderMinVer =
+            readSectionUint("bpfloader_min_ver", elfFile, DEFAULT_BPFLOADER_MIN_VER);
+    unsigned int bpfLoaderMaxVer =
+            readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
+    unsigned int bpfLoaderMinRequiredVer =
+            readSectionUint("bpfloader_min_required_ver", elfFile, 0);
+    size_t sizeOfBpfMapDef =
+            readSectionUint("size_of_bpf_map_def", elfFile, DEFAULT_SIZEOF_BPF_MAP_DEF);
+    size_t sizeOfBpfProgDef =
+            readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
+
+    // inclusive lower bound check
+    if (bpfloader_ver < bpfLoaderMinVer) {
+        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
+              bpfloader_ver, elfPath, bpfLoaderMinVer);
+        return 0;
+    }
+
+    // exclusive upper bound check
+    if (bpfloader_ver >= bpfLoaderMaxVer) {
+        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
+              bpfloader_ver, elfPath, bpfLoaderMaxVer);
+        return 0;
+    }
+
+    if (bpfloader_ver < bpfLoaderMinRequiredVer) {
+        ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
+              bpfloader_ver, elfPath, bpfLoaderMinRequiredVer);
+        return -1;
+    }
+
+    ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
+          bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
+
+    if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
+        ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
+              DEFAULT_SIZEOF_BPF_MAP_DEF);
+        return -1;
+    }
+
+    if (sizeOfBpfProgDef < DEFAULT_SIZEOF_BPF_PROG_DEF) {
+        ALOGE("sizeof(bpf_prog_def) of %zu is too small (< %d)", sizeOfBpfProgDef,
+              DEFAULT_SIZEOF_BPF_PROG_DEF);
+        return -1;
+    }
+
+    ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
+    if (ret) {
+        ALOGE("Couldn't read all code sections in %s", elfPath);
+        return ret;
+    }
+
+    /* Just for future debugging */
+    if (0) dumpAllCs(cs);
+
+    ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef, bpfloader_ver);
+    if (ret) {
+        ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
+        return ret;
+    }
+
+    for (int i = 0; i < (int)mapFds.size(); i++)
+        ALOGV("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
+
+    applyMapRelo(elfFile, mapFds, cs);
+
+    ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix, bpfloader_ver);
+    if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
+
+    return ret;
+}
+
+static bool exists(const char* const path) {
+    int v = access(path, F_OK);
+    if (!v) return true;
     if (errno == ENOENT) return false;
     ALOGE("FATAL: access(%s, F_OK) -> %d [%d:%s]", path, v, errno, strerror(errno));
     abort();  // can only hit this if permissions (likely selinux) are screwed up
 }
 
 
-const android::bpf::Location locations[] = {
+const Location locations[] = {
         // S+ Tethering mainline module (network_stack): tether offload
         {
                 .dir = "/apex/com.android.tethering/etc/bpf/",
@@ -97,7 +1266,7 @@
         },
 };
 
-int loadAllElfObjects(const android::bpf::Location& location) {
+static int loadAllElfObjects(const unsigned int bpfloader_ver, const Location& location) {
     int retVal = 0;
     DIR* dir;
     struct dirent* ent;
@@ -111,12 +1280,12 @@
             progPath += s;
 
             bool critical;
-            int ret = android::bpf::loadProg(progPath.c_str(), &critical, location);
+            int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location);
             if (ret) {
                 if (critical) retVal = ret;
                 ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
             } else {
-                ALOGI("Loaded object: %s", progPath.c_str());
+                ALOGD("Loaded object: %s", progPath.c_str());
             }
         }
         closedir(dir);
@@ -124,7 +1293,7 @@
     return retVal;
 }
 
-int createSysFsBpfSubDir(const char* const prefix) {
+static int createSysFsBpfSubDir(const char* const prefix) {
     if (*prefix) {
         mode_t prevUmask = umask(0);
 
@@ -147,8 +1316,8 @@
 // Technically 'value' doesn't need to be newline terminated, but it's best
 // to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
 // which is usually how kernel devs test the actual sysctl interfaces.
-int writeProcSysFile(const char *filename, const char *value) {
-    android::base::unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
+static int writeProcSysFile(const char *filename, const char *value) {
+    base::unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
     if (fd < 0) {
         const int err = errno;
         ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
@@ -171,10 +1340,8 @@
 
 #define APEX_MOUNT_POINT "/apex/com.android.tethering"
 const char * const platformBpfLoader = "/system/bin/bpfloader";
-const char * const platformNetBpfLoad = "/system/bin/netbpfload";
-const char * const apexNetBpfLoad = APEX_MOUNT_POINT "/bin/netbpfload";
 
-int logTetheringApexVersion(void) {
+static int logTetheringApexVersion(void) {
     char * found_blockdev = NULL;
     FILE * f = NULL;
     char buf[4096];
@@ -200,7 +1367,7 @@
     f = NULL;
 
     if (!found_blockdev) return 2;
-    ALOGD("Found Tethering Apex mounted from blockdev %s", found_blockdev);
+    ALOGV("Found Tethering Apex mounted from blockdev %s", found_blockdev);
 
     f = fopen("/proc/mounts", "re");
     if (!f) { free(found_blockdev); return 3; }
@@ -226,46 +1393,98 @@
     return 0;
 }
 
-int main(int argc, char** argv, char * const envp[]) {
-    (void)argc;
-    android::base::InitLogging(argv, &android::base::KernelLogger);
+static bool hasGSM() {
+    static string ph = base::GetProperty("gsm.current.phone-type", "");
+    static bool gsm = (ph != "");
+    static bool logged = false;
+    if (!logged) {
+        logged = true;
+        ALOGI("hasGSM(gsm.current.phone-type='%s'): %s", ph.c_str(), gsm ? "true" : "false");
+    }
+    return gsm;
+}
 
-    ALOGI("NetBpfLoad '%s' starting...", argv[0]);
+static bool isTV() {
+    if (hasGSM()) return false;  // TVs don't do GSM
 
-    // true iff we are running from the module
-    const bool is_mainline = !strcmp(argv[0], apexNetBpfLoad);
+    static string key = base::GetProperty("ro.oem.key1", "");
+    static bool tv = StartsWith(key, "ATV00");
+    static bool logged = false;
+    if (!logged) {
+        logged = true;
+        ALOGI("isTV(ro.oem.key1='%s'): %s.", key.c_str(), tv ? "true" : "false");
+    }
+    return tv;
+}
 
-    // true iff we are running from the platform
-    const bool is_platform = !strcmp(argv[0], platformNetBpfLoad);
+static bool isWear() {
+    static string wearSdkStr = base::GetProperty("ro.cw_build.wear_sdk.version", "");
+    static int wearSdkInt = base::GetIntProperty("ro.cw_build.wear_sdk.version", 0);
+    static string buildChars = base::GetProperty("ro.build.characteristics", "");
+    static vector<string> v = base::Tokenize(buildChars, ",");
+    static bool watch = (std::find(v.begin(), v.end(), "watch") != v.end());
+    static bool wear = (wearSdkInt > 0) || watch;
+    static bool logged = false;
+    if (!logged) {
+        logged = true;
+        ALOGI("isWear(ro.cw_build.wear_sdk.version=%d[%s] ro.build.characteristics='%s'): %s",
+              wearSdkInt, wearSdkStr.c_str(), buildChars.c_str(), wear ? "true" : "false");
+    }
+    return wear;
+}
 
-    const int device_api_level = android_get_device_api_level();
-    const bool isAtLeastT = (device_api_level >= __ANDROID_API_T__);
-    const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
-    const bool isAtLeastV = (device_api_level >= __ANDROID_API_V__);
+static int doLoad(char** argv, char * const envp[]) {
+    const bool runningAsRoot = !getuid();  // true iff U QPR3 or V+
+
+    // Any released device will have codename REL instead of a 'real' codename.
+    // For safety: default to 'REL' so we default to unreleased=false on failure.
+    const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
+
+    // goog/main device_api_level is bumped *way* before aosp/main api level
+    // (the latter only gets bumped during the push of goog/main to aosp/main)
+    //
+    // Since we develop in AOSP, we want it to behave as if it was bumped too.
+    //
+    // Note that AOSP doesn't really have a good api level (for example during
+    // early V dev cycle, it would have *all* of T, some but not all of U, and some V).
+    // One could argue that for our purposes AOSP api level should be infinite or 10000.
+    //
+    // This could also cause api to be increased in goog/main or other branches,
+    // but I can't imagine a case where this would be a problem: the problem
+    // is rather a too low api level, rather than some ill defined high value.
+    // For example as I write this aosp is 34/U, and goog is 35/V,
+    // we want to treat both goog & aosp as 35/V, but it's harmless if we
+    // treat goog as 36 because that value isn't yet defined to mean anything,
+    // and we thus never compare against it.
+    //
+    // Also note that 'android_get_device_api_level()' is what the
+    //   //system/core/init/apex_init_util.cpp
+    // apex init .XXrc parsing code uses for XX filtering.
+    //
+    // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
+    // but could (should?) perhaps be adjusted to match this.
+    const int effective_api_level = android_get_device_api_level() + (int)unreleased;
+    const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
+    const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
+    const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
 
     // last in U QPR2 beta1
     const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
     // first in U QPR2 beta~2
     const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
 
-    ALOGI("NetBpfLoad api:%d/%d kver:%07x platform:%d mainline:%d rc:%d%d",
-          android_get_application_target_sdk_version(), device_api_level,
-          android::bpf::kernelVersion(), is_platform, is_mainline,
+    // Version of Network BpfLoader depends on the Android OS version
+    unsigned int bpfloader_ver = 42u;    // [42] BPFLOADER_MAINLINE_VERSION
+    if (isAtLeastT) ++bpfloader_ver;     // [43] BPFLOADER_MAINLINE_T_VERSION
+    if (isAtLeastU) ++bpfloader_ver;     // [44] BPFLOADER_MAINLINE_U_VERSION
+    if (runningAsRoot) ++bpfloader_ver;  // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
+    if (isAtLeastV) ++bpfloader_ver;     // [46] BPFLOADER_MAINLINE_V_VERSION
+
+    ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
+          bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
+          kernelVersion(), describeArch(), getuid(),
           has_platform_bpfloader_rc, has_platform_netbpfload_rc);
 
-    if (!is_platform && !is_mainline) {
-        ALOGE("Unable to determine if we're platform or mainline netbpfload.");
-        return 1;
-    }
-
-    if (is_platform) {
-        ALOGI("Executing apex netbpfload...");
-        const char * args[] = { apexNetBpfLoad, NULL, };
-        execve(args[0], (char**)args, envp);
-        ALOGE("exec '%s' fail: %d[%s]", apexNetBpfLoad, errno, strerror(errno));
-        return 1;
-    }
-
     if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
         ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
         return 1;
@@ -278,37 +1497,72 @@
 
     logTetheringApexVersion();
 
-    if (is_mainline && has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
-        // Tethering apex shipped initrc file causes us to reach here
-        // but we're not ready to correctly handle anything before U QPR2
-        // in which the 'bpfloader' vs 'netbpfload' split happened
-        const char * args[] = { platformBpfLoader, NULL, };
-        execve(args[0], (char**)args, envp);
-        ALOGE("exec '%s' fail: %d[%s]", platformBpfLoader, errno, strerror(errno));
+    if (!isAtLeastT) {
+        ALOGE("Impossible - not reachable on Android <T.");
         return 1;
     }
 
-    if (isAtLeastT && !android::bpf::isAtLeastKernelVersion(4, 9, 0)) {
+    // both S and T require kernel 4.9 (and eBpf support)
+    if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
         ALOGE("Android T requires kernel 4.9.");
         return 1;
     }
 
-    if (isAtLeastU && !android::bpf::isAtLeastKernelVersion(4, 14, 0)) {
+    // U bumps the kernel requirement up to 4.14
+    if (isAtLeastU && !isAtLeastKernelVersion(4, 14, 0)) {
         ALOGE("Android U requires kernel 4.14.");
         return 1;
     }
 
-    if (isAtLeastV && !android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
+    // V bumps the kernel requirement up to 4.19
+    // see also: //system/netd/tests/kernel_test.cpp TestKernel419
+    if (isAtLeastV && !isAtLeastKernelVersion(4, 19, 0)) {
         ALOGE("Android V requires kernel 4.19.");
         return 1;
     }
 
-    if (isAtLeastV && android::bpf::isX86() && !android::bpf::isKernel64Bit()) {
-        ALOGE("Android V requires X86 kernel to be 64-bit.");
-        return 1;
+    // Technically already required by U, but only enforce on V+
+    // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
+    if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
+        ALOGE("Android V+ platform with 32 bit kernel version >= 5.16.0 is unsupported");
+        if (!isTV()) return 1;
     }
 
-    if (android::bpf::isUserspace32bit() && android::bpf::isAtLeastKernelVersion(6, 2, 0)) {
+    // Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
+    if (isAtLeastV && isKernel32Bit() && isX86()) {
+        ALOGE("Android V requires X86 kernel to be 64-bit.");
+        if (!isTV()) return 1;
+    }
+
+    if (isAtLeastV) {
+        bool bad = false;
+
+        if (!isLtsKernel()) {
+            ALOGW("Android V only supports LTS kernels.");
+            bad = true;
+        }
+
+#define REQUIRE(maj, min, sub) \
+        if (isKernelVersion(maj, min) && !isAtLeastKernelVersion(maj, min, sub)) { \
+            ALOGW("Android V requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
+            bad = true; \
+        }
+
+        REQUIRE(4, 19, 236)
+        REQUIRE(5, 4, 186)
+        REQUIRE(5, 10, 199)
+        REQUIRE(5, 15, 136)
+        REQUIRE(6, 1, 57)
+        REQUIRE(6, 6, 0)
+
+#undef REQUIRE
+
+        if (bad) {
+            ALOGE("Unsupported kernel version (%07x).", kernelVersion());
+        }
+    }
+
+    if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
         /* Android 14/U should only launch on 64-bit kernels
          *   T launches on 5.10/5.15
          *   U launches on 5.15/6.1
@@ -324,28 +1578,42 @@
          * Some of these have userspace or kernel workarounds/hacks.
          * Some of them don't...
          * We're going to be removing the hacks.
+         * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
+         * Note: this check/enforcement only applies to *system* userspace code,
+         * it does not affect unprivileged apps, the 32-on-64 compatibility
+         * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
          *
          * Additionally the 32-bit kernel jit support is poor,
          * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
          */
         ALOGE("64-bit userspace required on 6.2+ kernels.");
-        return 1;
+        // Stuff won't work reliably, but exempt TVs & Arm Wear devices
+        if (!isTV() && !(isWear() && isArm())) return 1;
     }
 
     // Ensure we can determine the Android build type.
-    if (!android::bpf::isEng() && !android::bpf::isUser() && !android::bpf::isUserdebug()) {
+    if (!isEng() && !isUser() && !isUserdebug()) {
         ALOGE("Failed to determine the build type: got %s, want 'eng', 'user', or 'userdebug'",
-              android::bpf::getBuildType().c_str());
+              getBuildType().c_str());
         return 1;
     }
 
-    if (isAtLeastU) {
+    if (runningAsRoot) {
+        // Note: writing this proc file requires being root (always the case on V+)
+
         // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
         // but we need 0 (enabled)
         // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
         // pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
         if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
-            android::bpf::isAtLeastKernelVersion(5, 13, 0)) return 1;
+            isAtLeastKernelVersion(5, 13, 0)) return 1;
+    }
+
+    if (isAtLeastU) {
+        // Note: writing these proc files requires CAP_NET_ADMIN
+        // and sepolicy which is only present on U+,
+        // on Android T and earlier versions they're written from the 'load_bpf_programs'
+        // trigger (ie. by init itself) instead.
 
         // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
         // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
@@ -378,7 +1646,7 @@
 
     // Load all ELF objects, create programs and maps, and pin them
     for (const auto& location : locations) {
-        if (loadAllElfObjects(location) != 0) {
+        if (loadAllElfObjects(bpfloader_ver, location) != 0) {
             ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
             ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
             ALOGE("If this triggers randomly, you might be hitting some memory allocation "
@@ -391,17 +1659,49 @@
 
     int key = 1;
     int value = 123;
-    android::base::unique_fd map(
-            android::bpf::createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
-    if (android::bpf::writeToMapEntry(map, &key, &value, BPF_ANY)) {
+    base::unique_fd map(
+            createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+    if (writeToMapEntry(map, &key, &value, BPF_ANY)) {
         ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
         return 1;
     }
 
+    // leave a flag that we're done
+    if (createSysFsBpfSubDir("netd_shared/mainline_done")) return 1;
+
+    // platform bpfloader will only succeed when run as root
+    if (!runningAsRoot) {
+        // unreachable on U QPR3+ which always runs netbpfload as root
+
+        ALOGI("mainline done, no need to transfer control to platform bpf loader.");
+        return 0;
+    }
+
+    // unreachable before U QPR3
     ALOGI("done, transferring control to platform bpfloader.");
 
+    // platform BpfLoader *needs* to run as root
     const char * args[] = { platformBpfLoader, NULL, };
     execve(args[0], (char**)args, envp);
     ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
     return 1;
 }
+
+}  // namespace bpf
+}  // namespace android
+
+int main(int argc, char** argv, char * const envp[]) {
+    android::base::InitLogging(argv, &android::base::KernelLogger);
+
+    if (argc == 2 && !strcmp(argv[1], "done")) {
+        // we're being re-exec'ed from platform bpfloader to 'finalize' things
+        if (!android::base::SetProperty("bpf.progs_loaded", "1")) {
+            ALOGE("Failed to set bpf.progs_loaded property to 1.");
+            return 125;
+        }
+        ALOGI("success.");
+        return 0;
+    }
+
+    return android::bpf::doLoad(argv, envp);
+}
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
deleted file mode 100644
index c534b2c..0000000
--- a/netbpfload/loader.cpp
+++ /dev/null
@@ -1,1185 +0,0 @@
-/*
- * Copyright (C) 2018-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.
- */
-
-#define LOG_TAG "NetBpfLoader"
-
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/bpf.h>
-#include <linux/elf.h>
-#include <log/log.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sysexits.h>
-#include <sys/stat.h>
-#include <sys/utsname.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-// This is BpfLoader v0.41
-// WARNING: If you ever hit cherrypick conflicts here you're doing it wrong:
-// You are NOT allowed to cherrypick bpfloader related patches out of order.
-// (indeed: cherrypicking is probably a bad idea and you should merge instead)
-// Mainline supports ONLY the published versions of the bpfloader for each Android release.
-#define BPFLOADER_VERSION_MAJOR 0u
-#define BPFLOADER_VERSION_MINOR 41u
-#define BPFLOADER_VERSION ((BPFLOADER_VERSION_MAJOR << 16) | BPFLOADER_VERSION_MINOR)
-
-#include "BpfSyscallWrappers.h"
-#include "bpf/BpfUtils.h"
-#include "bpf/bpf_map_def.h"
-#include "loader.h"
-
-#if BPFLOADER_VERSION < COMPILE_FOR_BPFLOADER_VERSION
-#error "BPFLOADER_VERSION is less than COMPILE_FOR_BPFLOADER_VERSION"
-#endif
-
-#include <cstdlib>
-#include <fstream>
-#include <iostream>
-#include <optional>
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-#include <android-base/cmsg.h>
-#include <android-base/file.h>
-#include <android-base/properties.h>
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
-
-#define BPF_FS_PATH "/sys/fs/bpf/"
-
-// Size of the BPF log buffer for verifier logging
-#define BPF_LOAD_LOG_SZ 0xfffff
-
-// Unspecified attach type is 0 which is BPF_CGROUP_INET_INGRESS.
-#define BPF_ATTACH_TYPE_UNSPEC BPF_CGROUP_INET_INGRESS
-
-using android::base::StartsWith;
-using android::base::unique_fd;
-using std::ifstream;
-using std::ios;
-using std::optional;
-using std::string;
-using std::vector;
-
-namespace android {
-namespace bpf {
-
-const std::string& getBuildType() {
-    static std::string t = android::base::GetProperty("ro.build.type", "unknown");
-    return t;
-}
-
-static unsigned int page_size = static_cast<unsigned int>(getpagesize());
-
-constexpr const char* lookupSelinuxContext(const domain d, const char* const unspecified = "") {
-    switch (d) {
-        case domain::unspecified:   return unspecified;
-        case domain::tethering:     return "fs_bpf_tethering";
-        case domain::net_private:   return "fs_bpf_net_private";
-        case domain::net_shared:    return "fs_bpf_net_shared";
-        case domain::netd_readonly: return "fs_bpf_netd_readonly";
-        case domain::netd_shared:   return "fs_bpf_netd_shared";
-        default:                    return "(unrecognized)";
-    }
-}
-
-domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
-    for (domain d : AllDomains) {
-        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
-        if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
-        if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
-    }
-    ALOGW("ignoring unrecognized selinux_context '%-32s'", s);
-    // We should return 'unrecognized' here, however: returning unspecified will
-    // result in the system simply using the default context, which in turn
-    // will allow future expansion by adding more restrictive selinux types.
-    // Older bpfloader will simply ignore that, and use the less restrictive default.
-    // This does mean you CANNOT later add a *less* restrictive type than the default.
-    //
-    // Note: we cannot just abort() here as this might be a mainline module shipped optional update
-    return domain::unspecified;
-}
-
-constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
-    switch (d) {
-        case domain::unspecified:   return unspecified;
-        case domain::tethering:     return "tethering/";
-        case domain::net_private:   return "net_private/";
-        case domain::net_shared:    return "net_shared/";
-        case domain::netd_readonly: return "netd_readonly/";
-        case domain::netd_shared:   return "netd_shared/";
-        default:                    return "(unrecognized)";
-    }
-};
-
-domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
-    for (domain d : AllDomains) {
-        // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
-        if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
-        if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
-    }
-    ALOGE("unrecognized pin_subdir '%-32s'", s);
-    // pin_subdir affects the object's full pathname,
-    // and thus using the default would change the location and thus our code's ability to find it,
-    // hence this seems worth treating as a true error condition.
-    //
-    // Note: we cannot just abort() here as this might be a mainline module shipped optional update
-    // However, our callers will treat this as an error, and stop loading the specific .o,
-    // which will fail bpfloader if the .o is marked critical.
-    return domain::unrecognized;
-}
-
-static string pathToObjName(const string& path) {
-    // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
-    string filename = android::base::Split(path, "/").back();
-    // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
-    string name = filename.substr(0, filename.find_last_of('.'));
-    // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
-    // this can be used to provide duplicate programs (mux based on the bpfloader version)
-    return name.substr(0, name.find_last_of('@'));
-}
-
-typedef struct {
-    const char* name;
-    enum bpf_prog_type type;
-    enum bpf_attach_type expected_attach_type;
-} sectionType;
-
-/*
- * Map section name prefixes to program types, the section name will be:
- *   SECTION(<prefix>/<name-of-program>)
- * For example:
- *   SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
- * is the name of the program, and tracepoint is the type.
- *
- * However, be aware that you should not be directly using the SECTION() macro.
- * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
- *
- * Programs shipped inside the tethering apex should be limited to networking stuff,
- * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
- * since they are less stable abi/api and may conflict with platform uses of bpf.
- */
-sectionType sectionNameTypes[] = {
-        {"bind4/",         BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
-        {"bind6/",         BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
-        {"cgroupskb/",     BPF_PROG_TYPE_CGROUP_SKB,       BPF_ATTACH_TYPE_UNSPEC},
-        {"cgroupsock/",    BPF_PROG_TYPE_CGROUP_SOCK,      BPF_ATTACH_TYPE_UNSPEC},
-        {"connect4/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
-        {"connect6/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
-        {"egress/",        BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_EGRESS},
-        {"getsockopt/",    BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_GETSOCKOPT},
-        {"ingress/",       BPF_PROG_TYPE_CGROUP_SKB,       BPF_CGROUP_INET_INGRESS},
-        {"lwt_in/",        BPF_PROG_TYPE_LWT_IN,           BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_out/",       BPF_PROG_TYPE_LWT_OUT,          BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_seg6local/", BPF_PROG_TYPE_LWT_SEG6LOCAL,    BPF_ATTACH_TYPE_UNSPEC},
-        {"lwt_xmit/",      BPF_PROG_TYPE_LWT_XMIT,         BPF_ATTACH_TYPE_UNSPEC},
-        {"postbind4/",     BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET4_POST_BIND},
-        {"postbind6/",     BPF_PROG_TYPE_CGROUP_SOCK,      BPF_CGROUP_INET6_POST_BIND},
-        {"recvmsg4/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
-        {"recvmsg6/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
-        {"schedact/",      BPF_PROG_TYPE_SCHED_ACT,        BPF_ATTACH_TYPE_UNSPEC},
-        {"schedcls/",      BPF_PROG_TYPE_SCHED_CLS,        BPF_ATTACH_TYPE_UNSPEC},
-        {"sendmsg4/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
-        {"sendmsg6/",      BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
-        {"setsockopt/",    BPF_PROG_TYPE_CGROUP_SOCKOPT,   BPF_CGROUP_SETSOCKOPT},
-        {"skfilter/",      BPF_PROG_TYPE_SOCKET_FILTER,    BPF_ATTACH_TYPE_UNSPEC},
-        {"sockops/",       BPF_PROG_TYPE_SOCK_OPS,         BPF_CGROUP_SOCK_OPS},
-        {"sysctl",         BPF_PROG_TYPE_CGROUP_SYSCTL,    BPF_CGROUP_SYSCTL},
-        {"xdp/",           BPF_PROG_TYPE_XDP,              BPF_ATTACH_TYPE_UNSPEC},
-};
-
-typedef struct {
-    enum bpf_prog_type type;
-    enum bpf_attach_type expected_attach_type;
-    string name;
-    vector<char> data;
-    vector<char> rel_data;
-    optional<struct bpf_prog_def> prog_def;
-
-    unique_fd prog_fd; /* fd after loading */
-} codeSection;
-
-static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
-    elfFile.seekg(0);
-    if (elfFile.fail()) return -1;
-
-    if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
-
-    return 0;
-}
-
-/* Reads all section header tables into an Shdr array */
-static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
-    Elf64_Ehdr eh;
-    int ret = 0;
-
-    ret = readElfHeader(elfFile, &eh);
-    if (ret) return ret;
-
-    elfFile.seekg(eh.e_shoff);
-    if (elfFile.fail()) return -1;
-
-    /* Read shdr table entries */
-    shTable.resize(eh.e_shnum);
-
-    if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
-
-    return 0;
-}
-
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
-    vector<Elf64_Shdr> shTable;
-    int ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-
-    elfFile.seekg(shTable[id].sh_offset);
-    if (elfFile.fail()) return -1;
-
-    sec.resize(shTable[id].sh_size);
-    if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
-
-    return 0;
-}
-
-/* Read whole section header string table */
-static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
-    Elf64_Ehdr eh;
-    int ret = readElfHeader(elfFile, &eh);
-    if (ret) return ret;
-
-    ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
-    if (ret) return ret;
-
-    return 0;
-}
-
-/* Get name from offset in strtab */
-static int getSymName(ifstream& elfFile, int nameOff, string& name) {
-    int ret;
-    vector<char> secStrTab;
-
-    ret = readSectionHeaderStrtab(elfFile, secStrTab);
-    if (ret) return ret;
-
-    if (nameOff >= (int)secStrTab.size()) return -1;
-
-    name = string((char*)secStrTab.data() + nameOff);
-    return 0;
-}
-
-/* Reads a full section by name - example to get the GPL license */
-static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
-    vector<char> secStrTab;
-    vector<Elf64_Shdr> shTable;
-    int ret;
-
-    ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-
-    ret = readSectionHeaderStrtab(elfFile, secStrTab);
-    if (ret) return ret;
-
-    for (int i = 0; i < (int)shTable.size(); i++) {
-        char* secname = secStrTab.data() + shTable[i].sh_name;
-        if (!secname) continue;
-
-        if (!strcmp(secname, name)) {
-            vector<char> dataTmp;
-            dataTmp.resize(shTable[i].sh_size);
-
-            elfFile.seekg(shTable[i].sh_offset);
-            if (elfFile.fail()) return -1;
-
-            if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
-
-            data = dataTmp;
-            return 0;
-        }
-    }
-    return -2;
-}
-
-unsigned int readSectionUint(const char* name, ifstream& elfFile, unsigned int defVal) {
-    vector<char> theBytes;
-    int ret = readSectionByName(name, elfFile, theBytes);
-    if (ret) {
-        ALOGD("Couldn't find section %s (defaulting to %u [0x%x]).", name, defVal, defVal);
-        return defVal;
-    } else if (theBytes.size() < sizeof(unsigned int)) {
-        ALOGE("Section %s too short (defaulting to %u [0x%x]).", name, defVal, defVal);
-        return defVal;
-    } else {
-        // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
-        unsigned int value = static_cast<unsigned char>(theBytes[3]);
-        value <<= 8;
-        value += static_cast<unsigned char>(theBytes[2]);
-        value <<= 8;
-        value += static_cast<unsigned char>(theBytes[1]);
-        value <<= 8;
-        value += static_cast<unsigned char>(theBytes[0]);
-        ALOGI("Section %s value is %u [0x%x]", name, value, value);
-        return value;
-    }
-}
-
-static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
-    int ret;
-    vector<Elf64_Shdr> shTable;
-
-    ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-
-    for (int i = 0; i < (int)shTable.size(); i++) {
-        if ((int)shTable[i].sh_type != type) continue;
-
-        vector<char> dataTmp;
-        dataTmp.resize(shTable[i].sh_size);
-
-        elfFile.seekg(shTable[i].sh_offset);
-        if (elfFile.fail()) return -1;
-
-        if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
-
-        data = dataTmp;
-        return 0;
-    }
-    return -2;
-}
-
-static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
-    return (a.st_value < b.st_value);
-}
-
-static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
-    int ret, numElems;
-    Elf64_Sym* buf;
-    vector<char> secData;
-
-    ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
-    if (ret) return ret;
-
-    buf = (Elf64_Sym*)secData.data();
-    numElems = (secData.size() / sizeof(Elf64_Sym));
-    data.assign(buf, buf + numElems);
-
-    if (sort) std::sort(data.begin(), data.end(), symCompare);
-    return 0;
-}
-
-static enum bpf_prog_type getSectionType(string& name) {
-    for (auto& snt : sectionNameTypes)
-        if (StartsWith(name, snt.name)) return snt.type;
-
-    return BPF_PROG_TYPE_UNSPEC;
-}
-
-static enum bpf_attach_type getExpectedAttachType(string& name) {
-    for (auto& snt : sectionNameTypes)
-        if (StartsWith(name, snt.name)) return snt.expected_attach_type;
-    return BPF_ATTACH_TYPE_UNSPEC;
-}
-
-/*
-static string getSectionName(enum bpf_prog_type type)
-{
-    for (auto& snt : sectionNameTypes)
-        if (snt.type == type)
-            return string(snt.name);
-
-    return "UNKNOWN SECTION NAME " + std::to_string(type);
-}
-*/
-
-static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
-                        size_t sizeOfBpfProgDef) {
-    vector<char> pdData;
-    int ret = readSectionByName("progs", elfFile, pdData);
-    // Older file formats do not require a 'progs' section at all.
-    // (We should probably figure out whether this is behaviour which is safe to remove now.)
-    if (ret == -2) return 0;
-    if (ret) return ret;
-
-    if (pdData.size() % sizeOfBpfProgDef) {
-        ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
-              pdData.size(), sizeOfBpfProgDef);
-        return -1;
-    };
-
-    int progCount = pdData.size() / sizeOfBpfProgDef;
-    pd.resize(progCount);
-    size_t trimmedSize = std::min(sizeOfBpfProgDef, sizeof(struct bpf_prog_def));
-
-    const char* dataPtr = pdData.data();
-    for (auto& p : pd) {
-        // First we zero initialize
-        memset(&p, 0, sizeof(p));
-        // Then we set non-zero defaults
-        p.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
-        // Then we copy over the structure prefix from the ELF file.
-        memcpy(&p, dataPtr, trimmedSize);
-        // Move to next struct in the ELF file
-        dataPtr += sizeOfBpfProgDef;
-    }
-    return 0;
-}
-
-static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
-                              optional<unsigned> symbolType = std::nullopt) {
-    int ret;
-    string name;
-    vector<Elf64_Sym> symtab;
-    vector<Elf64_Shdr> shTable;
-
-    ret = readSymTab(elfFile, 1 /* sort */, symtab);
-    if (ret) return ret;
-
-    /* Get index of section */
-    ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-
-    int sec_idx = -1;
-    for (int i = 0; i < (int)shTable.size(); i++) {
-        ret = getSymName(elfFile, shTable[i].sh_name, name);
-        if (ret) return ret;
-
-        if (!name.compare(sectionName)) {
-            sec_idx = i;
-            break;
-        }
-    }
-
-    /* No section found with matching name*/
-    if (sec_idx == -1) {
-        ALOGW("No %s section could be found in elf object", sectionName.c_str());
-        return -1;
-    }
-
-    for (int i = 0; i < (int)symtab.size(); i++) {
-        if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
-
-        if (symtab[i].st_shndx == sec_idx) {
-            string s;
-            ret = getSymName(elfFile, symtab[i].st_name, s);
-            if (ret) return ret;
-            names.push_back(s);
-        }
-    }
-
-    return 0;
-}
-
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
-    vector<Elf64_Shdr> shTable;
-    int entries, ret = 0;
-
-    ret = readSectionHeadersAll(elfFile, shTable);
-    if (ret) return ret;
-    entries = shTable.size();
-
-    vector<struct bpf_prog_def> pd;
-    ret = readProgDefs(elfFile, pd, sizeOfBpfProgDef);
-    if (ret) return ret;
-    vector<string> progDefNames;
-    ret = getSectionSymNames(elfFile, "progs", progDefNames);
-    if (!pd.empty() && ret) return ret;
-
-    for (int i = 0; i < entries; i++) {
-        string name;
-        codeSection cs_temp;
-        cs_temp.type = BPF_PROG_TYPE_UNSPEC;
-
-        ret = getSymName(elfFile, shTable[i].sh_name, name);
-        if (ret) return ret;
-
-        enum bpf_prog_type ptype = getSectionType(name);
-
-        if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
-
-        // This must be done before '/' is replaced with '_'.
-        cs_temp.expected_attach_type = getExpectedAttachType(name);
-
-        string oldName = name;
-
-        // convert all slashes to underscores
-        std::replace(name.begin(), name.end(), '/', '_');
-
-        cs_temp.type = ptype;
-        cs_temp.name = name;
-
-        ret = readSectionByIdx(elfFile, i, cs_temp.data);
-        if (ret) return ret;
-        ALOGD("Loaded code section %d (%s)", i, name.c_str());
-
-        vector<string> csSymNames;
-        ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
-        if (ret || !csSymNames.size()) return ret;
-        for (size_t i = 0; i < progDefNames.size(); ++i) {
-            if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
-                cs_temp.prog_def = pd[i];
-                break;
-            }
-        }
-
-        /* Check for rel section */
-        if (cs_temp.data.size() > 0 && i < entries) {
-            ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
-            if (ret) return ret;
-
-            if (name == (".rel" + oldName)) {
-                ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
-                if (ret) return ret;
-                ALOGD("Loaded relo section %d (%s)", i, name.c_str());
-            }
-        }
-
-        if (cs_temp.data.size() > 0) {
-            cs.push_back(std::move(cs_temp));
-            ALOGD("Adding section %d to cs list", i);
-        }
-    }
-    return 0;
-}
-
-static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
-    vector<Elf64_Sym> symtab;
-    int ret = 0;
-
-    ret = readSymTab(elfFile, 0 /* !sort */, symtab);
-    if (ret) return ret;
-
-    if (index >= (int)symtab.size()) return -1;
-
-    return getSymName(elfFile, symtab[index].st_name, name);
-}
-
-static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
-                                   const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
-    // Assuming fd is a valid Bpf Map file descriptor then
-    // all the following should always succeed on a 4.14+ kernel.
-    // If they somehow do fail, they'll return -1 (and set errno),
-    // which should then cause (among others) a key_size mismatch.
-    int fd_type = bpfGetFdMapType(fd);
-    int fd_key_size = bpfGetFdKeySize(fd);
-    int fd_value_size = bpfGetFdValueSize(fd);
-    int fd_max_entries = bpfGetFdMaxEntries(fd);
-    int fd_map_flags = bpfGetFdMapFlags(fd);
-
-    // DEVMAPs are readonly from the bpf program side's point of view, as such
-    // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
-    int desired_map_flags = (int)mapDef.map_flags;
-    if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
-        desired_map_flags |= BPF_F_RDONLY_PROG;
-
-    // The .h file enforces that this is a power of two, and page size will
-    // also always be a power of two, so this logic is actually enough to
-    // force it to be a multiple of the page size, as required by the kernel.
-    unsigned int desired_max_entries = mapDef.max_entries;
-    if (type == BPF_MAP_TYPE_RINGBUF) {
-        if (desired_max_entries < page_size) desired_max_entries = page_size;
-    }
-
-    // The following checks should *never* trigger, if one of them somehow does,
-    // it probably means a bpf .o file has been changed/replaced at runtime
-    // and bpfloader was manually rerun (normally it should only run *once*
-    // early during the boot process).
-    // Another possibility is that something is misconfigured in the code:
-    // most likely a shared map is declared twice differently.
-    // But such a change should never be checked into the source tree...
-    if ((fd_type == type) &&
-        (fd_key_size == (int)mapDef.key_size) &&
-        (fd_value_size == (int)mapDef.value_size) &&
-        (fd_max_entries == (int)desired_max_entries) &&
-        (fd_map_flags == desired_map_flags)) {
-        return true;
-    }
-
-    ALOGE("bpf map name %s mismatch: desired/found: "
-          "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
-          mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
-          fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
-    return false;
-}
-
-static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
-                      const char* prefix, const size_t sizeOfBpfMapDef) {
-    int ret;
-    vector<char> mdData;
-    vector<struct bpf_map_def> md;
-    vector<string> mapNames;
-    string objName = pathToObjName(string(elfPath));
-
-    ret = readSectionByName("maps", elfFile, mdData);
-    if (ret == -2) return 0;  // no maps to read
-    if (ret) return ret;
-
-    if (mdData.size() % sizeOfBpfMapDef) {
-        ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
-              mdData.size(), sizeOfBpfMapDef);
-        return -1;
-    };
-
-    int mapCount = mdData.size() / sizeOfBpfMapDef;
-    md.resize(mapCount);
-    size_t trimmedSize = std::min(sizeOfBpfMapDef, sizeof(struct bpf_map_def));
-
-    const char* dataPtr = mdData.data();
-    for (auto& m : md) {
-        // First we zero initialize
-        memset(&m, 0, sizeof(m));
-        // Then we set non-zero defaults
-        m.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
-        m.max_kver = 0xFFFFFFFFu;                         // matches KVER_INF from bpf_helpers.h
-        // Then we copy over the structure prefix from the ELF file.
-        memcpy(&m, dataPtr, trimmedSize);
-        // Move to next struct in the ELF file
-        dataPtr += sizeOfBpfMapDef;
-    }
-
-    ret = getSectionSymNames(elfFile, "maps", mapNames);
-    if (ret) return ret;
-
-    unsigned kvers = kernelVersion();
-
-    for (int i = 0; i < (int)mapNames.size(); i++) {
-        if (md[i].zero != 0) abort();
-
-        if (BPFLOADER_VERSION < md[i].bpfloader_min_ver) {
-            ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
-                  md[i].bpfloader_min_ver);
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if (BPFLOADER_VERSION >= md[i].bpfloader_max_ver) {
-            ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
-                  md[i].bpfloader_max_ver);
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if (kvers < md[i].min_kver) {
-            ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
-                  mapNames[i].c_str(), kvers, md[i].min_kver);
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if (kvers >= md[i].max_kver) {
-            ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
-                  mapNames[i].c_str(), kvers, md[i].max_kver);
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
-            (md[i].ignore_on_userdebug && isUserdebug())) {
-            ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
-                  getBuildType().c_str());
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
-            (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
-            (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
-            (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
-            (isRiscV() && md[i].ignore_on_riscv64)) {
-            ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
-                  describeArch());
-            mapFds.push_back(unique_fd());
-            continue;
-        }
-
-        enum bpf_map_type type = md[i].type;
-        if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
-            // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
-            // of be approximated: HASH has the same userspace visible api.
-            // However it cannot be used by ebpf programs in the same way.
-            // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
-            // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
-            // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
-            // programs as being 5.4+...
-            type = BPF_MAP_TYPE_HASH;
-        }
-
-        // The .h file enforces that this is a power of two, and page size will
-        // also always be a power of two, so this logic is actually enough to
-        // force it to be a multiple of the page size, as required by the kernel.
-        unsigned int max_entries = md[i].max_entries;
-        if (type == BPF_MAP_TYPE_RINGBUF) {
-            if (max_entries < page_size) max_entries = page_size;
-        }
-
-        domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
-        if (specified(selinux_context)) {
-            ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
-                  md[i].selinux_context, selinux_context, lookupSelinuxContext(selinux_context),
-                  lookupPinSubdir(selinux_context));
-        }
-
-        domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
-        if (unrecognized(pin_subdir)) return -ENOTDIR;
-        if (specified(pin_subdir)) {
-            ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
-                  pin_subdir, lookupPinSubdir(pin_subdir));
-        }
-
-        // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
-        // except that maps shared across .o's have empty <objName>
-        // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
-        string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
-                           (md[i].shared ? "" : objName) + "_" + mapNames[i];
-        bool reuse = false;
-        unique_fd fd;
-        int saved_errno;
-
-        if (access(mapPinLoc.c_str(), F_OK) == 0) {
-            fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
-            saved_errno = errno;
-            ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
-            reuse = true;
-        } else {
-            union bpf_attr req = {
-              .map_type = type,
-              .key_size = md[i].key_size,
-              .value_size = md[i].value_size,
-              .max_entries = max_entries,
-              .map_flags = md[i].map_flags,
-            };
-            strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
-            fd.reset(bpf(BPF_MAP_CREATE, req));
-            saved_errno = errno;
-            ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
-        }
-
-        if (!fd.ok()) return -saved_errno;
-
-        // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
-        // safety (since reuse code path is rare) run these checks even if we just created it.
-        // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
-        if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
-
-        if (!reuse) {
-            if (specified(selinux_context)) {
-                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
-                                   "tmp_map_" + objName + "_" + mapNames[i];
-                ret = bpfFdPin(fd, createLoc.c_str());
-                if (ret) {
-                    int err = errno;
-                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
-                    return -err;
-                }
-                ret = renameat2(AT_FDCWD, createLoc.c_str(),
-                                AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
-                if (ret) {
-                    int err = errno;
-                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
-                          err, strerror(err));
-                    return -err;
-                }
-            } else {
-                ret = bpfFdPin(fd, mapPinLoc.c_str());
-                if (ret) {
-                    int err = errno;
-                    ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
-                    return -err;
-                }
-            }
-            ret = chmod(mapPinLoc.c_str(), md[i].mode);
-            if (ret) {
-                int err = errno;
-                ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
-                      strerror(err));
-                return -err;
-            }
-            ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
-            if (ret) {
-                int err = errno;
-                ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
-                      ret, err, strerror(err));
-                return -err;
-            }
-        }
-
-        int mapId = bpfGetFdMapId(fd);
-        if (mapId == -1) {
-            ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
-        } else {
-            ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
-        }
-
-        mapFds.push_back(std::move(fd));
-    }
-
-    return ret;
-}
-
-/* For debugging, dump all instructions */
-static void dumpIns(char* ins, int size) {
-    for (int row = 0; row < size / 8; row++) {
-        ALOGE("%d: ", row);
-        for (int j = 0; j < 8; j++) {
-            ALOGE("%3x ", ins[(row * 8) + j]);
-        }
-        ALOGE("\n");
-    }
-}
-
-/* For debugging, dump all code sections from cs list */
-static void dumpAllCs(vector<codeSection>& cs) {
-    for (int i = 0; i < (int)cs.size(); i++) {
-        ALOGE("Dumping cs %d, name %s", int(i), cs[i].name.c_str());
-        dumpIns((char*)cs[i].data.data(), cs[i].data.size());
-        ALOGE("-----------");
-    }
-}
-
-static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
-    int insnIndex;
-    struct bpf_insn *insn, *insns;
-
-    insns = (struct bpf_insn*)(insnsPtr);
-
-    insnIndex = offset / sizeof(struct bpf_insn);
-    insn = &insns[insnIndex];
-
-    // Occasionally might be useful for relocation debugging, but pretty spammy
-    if (0) {
-        ALOGD("applying relo to instruction at byte offset: %llu, "
-              "insn offset %d, insn %llx",
-              (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
-    }
-
-    if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
-        ALOGE("Dumping all instructions till ins %d", insnIndex);
-        ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
-        dumpIns((char*)insnsPtr, (insnIndex + 3) * 8);
-        return;
-    }
-
-    insn->imm = fd;
-    insn->src_reg = BPF_PSEUDO_MAP_FD;
-}
-
-static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
-    vector<string> mapNames;
-
-    int ret = getSectionSymNames(elfFile, "maps", mapNames);
-    if (ret) return;
-
-    for (int k = 0; k != (int)cs.size(); k++) {
-        Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
-        int n_rel = cs[k].rel_data.size() / sizeof(*rel);
-
-        for (int i = 0; i < n_rel; i++) {
-            int symIndex = ELF64_R_SYM(rel[i].r_info);
-            string symName;
-
-            ret = getSymNameByIdx(elfFile, symIndex, symName);
-            if (ret) return;
-
-            /* Find the map fd and apply relo */
-            for (int j = 0; j < (int)mapNames.size(); j++) {
-                if (!mapNames[j].compare(symName)) {
-                    applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
-                    break;
-                }
-            }
-        }
-    }
-}
-
-static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
-                            const char* prefix) {
-    unsigned kvers = kernelVersion();
-
-    if (!kvers) {
-        ALOGE("unable to get kernel version");
-        return -EINVAL;
-    }
-
-    string objName = pathToObjName(string(elfPath));
-
-    for (int i = 0; i < (int)cs.size(); i++) {
-        unique_fd& fd = cs[i].prog_fd;
-        int ret;
-        string name = cs[i].name;
-
-        if (!cs[i].prog_def.has_value()) {
-            ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
-            return -EINVAL;
-        }
-
-        unsigned min_kver = cs[i].prog_def->min_kver;
-        unsigned max_kver = cs[i].prog_def->max_kver;
-        ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
-             max_kver, kvers);
-        if (kvers < min_kver) continue;
-        if (kvers >= max_kver) continue;
-
-        unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
-        unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
-        domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
-        domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
-        // Note: make sure to only check for unrecognized *after* verifying bpfloader
-        // version limits include this bpfloader's version.
-
-        ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
-              bpfMinVer, bpfMaxVer);
-        if (BPFLOADER_VERSION < bpfMinVer) continue;
-        if (BPFLOADER_VERSION >= bpfMaxVer) continue;
-
-        if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
-            (cs[i].prog_def->ignore_on_user && isUser()) ||
-            (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
-            ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
-                  getBuildType().c_str());
-            continue;
-        }
-
-        if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
-            (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
-            (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
-            (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
-            (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
-            ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
-            continue;
-        }
-
-        if (unrecognized(pin_subdir)) return -ENOTDIR;
-
-        if (specified(selinux_context)) {
-            ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
-                  cs[i].prog_def->selinux_context, selinux_context,
-                  lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
-        }
-
-        if (specified(pin_subdir)) {
-            ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
-                  cs[i].prog_def->pin_subdir, pin_subdir, lookupPinSubdir(pin_subdir));
-        }
-
-        // strip any potential $foo suffix
-        // this can be used to provide duplicate programs
-        // conditionally loaded based on running kernel version
-        name = name.substr(0, name.find_last_of('$'));
-
-        bool reuse = false;
-        // Format of pin location is
-        // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
-        string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
-                            objName + '_' + string(name);
-        if (access(progPinLoc.c_str(), F_OK) == 0) {
-            fd.reset(retrieveProgram(progPinLoc.c_str()));
-            ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
-                  (!fd.ok() ? std::strerror(errno) : "no error"));
-            reuse = true;
-        } else {
-            vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
-
-            union bpf_attr req = {
-              .prog_type = cs[i].type,
-              .kern_version = kvers,
-              .license = ptr_to_u64(license.c_str()),
-              .insns = ptr_to_u64(cs[i].data.data()),
-              .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
-              .log_level = 1,
-              .log_buf = ptr_to_u64(log_buf.data()),
-              .log_size = static_cast<__u32>(log_buf.size()),
-              .expected_attach_type = cs[i].expected_attach_type,
-            };
-            strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
-            fd.reset(bpf(BPF_PROG_LOAD, req));
-
-            ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
-                  cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
-
-            if (!fd.ok()) {
-                vector<string> lines = android::base::Split(log_buf.data(), "\n");
-
-                ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
-                for (const auto& line : lines) ALOGW("%s", line.c_str());
-                ALOGW("BPF_PROG_LOAD - END log_buf contents.");
-
-                if (cs[i].prog_def->optional) {
-                    ALOGW("failed program is marked optional - continuing...");
-                    continue;
-                }
-                ALOGE("non-optional program failed to load.");
-            }
-        }
-
-        if (!fd.ok()) return fd.get();
-
-        if (!reuse) {
-            if (specified(selinux_context)) {
-                string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
-                                   "tmp_prog_" + objName + '_' + string(name);
-                ret = bpfFdPin(fd, createLoc.c_str());
-                if (ret) {
-                    int err = errno;
-                    ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
-                    return -err;
-                }
-                ret = renameat2(AT_FDCWD, createLoc.c_str(),
-                                AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
-                if (ret) {
-                    int err = errno;
-                    ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
-                          err, strerror(err));
-                    return -err;
-                }
-            } else {
-                ret = bpfFdPin(fd, progPinLoc.c_str());
-                if (ret) {
-                    int err = errno;
-                    ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
-                    return -err;
-                }
-            }
-            if (chmod(progPinLoc.c_str(), 0440)) {
-                int err = errno;
-                ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
-                return -err;
-            }
-            if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
-                      (gid_t)cs[i].prog_def->gid)) {
-                int err = errno;
-                ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
-                      cs[i].prog_def->gid, err, strerror(err));
-                return -err;
-            }
-        }
-
-        int progId = bpfGetFdProgId(fd);
-        if (progId == -1) {
-            ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
-        } else {
-            ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
-        }
-    }
-
-    return 0;
-}
-
-int loadProg(const char* elfPath, bool* isCritical, const Location& location) {
-    vector<char> license;
-    vector<char> critical;
-    vector<codeSection> cs;
-    vector<unique_fd> mapFds;
-    int ret;
-
-    if (!isCritical) return -1;
-    *isCritical = false;
-
-    ifstream elfFile(elfPath, ios::in | ios::binary);
-    if (!elfFile.is_open()) return -1;
-
-    ret = readSectionByName("critical", elfFile, critical);
-    *isCritical = !ret;
-
-    ret = readSectionByName("license", elfFile, license);
-    if (ret) {
-        ALOGE("Couldn't find license in %s", elfPath);
-        return ret;
-    } else {
-        ALOGD("Loading %s%s ELF object %s with license %s",
-              *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
-              elfPath, (char*)license.data());
-    }
-
-    // the following default values are for bpfloader V0.0 format which does not include them
-    unsigned int bpfLoaderMinVer =
-            readSectionUint("bpfloader_min_ver", elfFile, DEFAULT_BPFLOADER_MIN_VER);
-    unsigned int bpfLoaderMaxVer =
-            readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
-    unsigned int bpfLoaderMinRequiredVer =
-            readSectionUint("bpfloader_min_required_ver", elfFile, 0);
-    size_t sizeOfBpfMapDef =
-            readSectionUint("size_of_bpf_map_def", elfFile, DEFAULT_SIZEOF_BPF_MAP_DEF);
-    size_t sizeOfBpfProgDef =
-            readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
-
-    // inclusive lower bound check
-    if (BPFLOADER_VERSION < bpfLoaderMinVer) {
-        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
-              BPFLOADER_VERSION, elfPath, bpfLoaderMinVer);
-        return 0;
-    }
-
-    // exclusive upper bound check
-    if (BPFLOADER_VERSION >= bpfLoaderMaxVer) {
-        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
-              BPFLOADER_VERSION, elfPath, bpfLoaderMaxVer);
-        return 0;
-    }
-
-    if (BPFLOADER_VERSION < bpfLoaderMinRequiredVer) {
-        ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
-              BPFLOADER_VERSION, elfPath, bpfLoaderMinRequiredVer);
-        return -1;
-    }
-
-    ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
-          BPFLOADER_VERSION, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
-
-    if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
-        ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
-              DEFAULT_SIZEOF_BPF_MAP_DEF);
-        return -1;
-    }
-
-    if (sizeOfBpfProgDef < DEFAULT_SIZEOF_BPF_PROG_DEF) {
-        ALOGE("sizeof(bpf_prog_def) of %zu is too small (< %d)", sizeOfBpfProgDef,
-              DEFAULT_SIZEOF_BPF_PROG_DEF);
-        return -1;
-    }
-
-    ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
-    if (ret) {
-        ALOGE("Couldn't read all code sections in %s", elfPath);
-        return ret;
-    }
-
-    /* Just for future debugging */
-    if (0) dumpAllCs(cs);
-
-    ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef);
-    if (ret) {
-        ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
-        return ret;
-    }
-
-    for (int i = 0; i < (int)mapFds.size(); i++)
-        ALOGD("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
-
-    applyMapRelo(elfFile, mapFds, cs);
-
-    ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix);
-    if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
-
-    return ret;
-}
-
-}  // namespace bpf
-}  // namespace android
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
deleted file mode 100644
index b884637..0000000
--- a/netbpfload/loader.h
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2018-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.
- */
-
-#pragma once
-
-#include <linux/bpf.h>
-
-#include <fstream>
-
-namespace android {
-namespace bpf {
-
-// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
-//
-// The BpfLoader needs to convert these bpf.o specified strings into an enum
-// for internal use (to check that valid values were specified for the specific
-// location of the bpf.o file).
-//
-// It also needs to map selinux_context's into pin_subdir's.
-// This is because of how selinux_context is actually implemented via pin+rename.
-//
-// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
-// is aware of.  Thus there currently needs to be a 1:1 mapping between the two.
-//
-enum class domain : int {
-    unrecognized = -1,  // invalid for this version of the bpfloader
-    unspecified = 0,    // means just use the default for that specific pin location
-    tethering,          // (S+) fs_bpf_tethering     /sys/fs/bpf/tethering
-    net_private,        // (T+) fs_bpf_net_private   /sys/fs/bpf/net_private
-    net_shared,         // (T+) fs_bpf_net_shared    /sys/fs/bpf/net_shared
-    netd_readonly,      // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
-    netd_shared,        // (T+) fs_bpf_netd_shared   /sys/fs/bpf/netd_shared
-};
-
-// Note: this does not include domain::unrecognized, but does include domain::unspecified
-static constexpr domain AllDomains[] = {
-    domain::unspecified,
-    domain::tethering,
-    domain::net_private,
-    domain::net_shared,
-    domain::netd_readonly,
-    domain::netd_shared,
-};
-
-static constexpr bool unrecognized(domain d) {
-    return d == domain::unrecognized;
-}
-
-// Note: this doesn't handle unrecognized, handle it first.
-static constexpr bool specified(domain d) {
-    return d != domain::unspecified;
-}
-
-struct Location {
-    const char* const dir = "";
-    const char* const prefix = "";
-};
-
-// BPF loader implementation. Loads an eBPF ELF object
-int loadProg(const char* elfPath, bool* isCritical, const Location &location = {});
-
-// Exposed for testing
-unsigned int readSectionUint(const char* name, std::ifstream& elfFile, unsigned int defVal);
-
-// Returns the build type string (from ro.build.type).
-const std::string& getBuildType();
-
-// The following functions classify the 3 Android build types.
-inline bool isEng() {
-    return getBuildType() == "eng";
-}
-inline bool isUser() {
-    return getBuildType() == "user";
-}
-inline bool isUserdebug() {
-    return getBuildType() == "userdebug";
-}
-
-}  // namespace bpf
-}  // namespace android
diff --git a/netbpfload/netbpfload.33rc b/netbpfload/netbpfload.33rc
new file mode 100644
index 0000000..493731f
--- /dev/null
+++ b/netbpfload/netbpfload.33rc
@@ -0,0 +1,21 @@
+# This file takes effect only on T and U (on V netbpfload.35rc takes priority).
+#
+# The service is started from netd's libnetd_updatable shared library
+# on initial (boot time) startup of netd.
+#
+# However we never start this service on U QPR3.
+#
+# This is due to lack of a need: U QPR2 split the previously single
+# platform bpfloader into platform netbpfload -> platform bpfloader.
+# U QPR3 made the platform netbpfload unconditionally exec apex netbpfload,
+# so by the time U QPR3's netd runs, apex netbpfload is already done.
+
+service mdnsd_netbpfload /apex/com.android.tethering/bin/netbpfload
+    capabilities CHOWN SYS_ADMIN NET_ADMIN
+    group system root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw
+    user system
+    file /dev/kmsg w
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    reboot_on_failure reboot,netbpfload-failed
+    override
diff --git a/netbpfload/netbpfload.mainline.rc b/netbpfload/netbpfload.35rc
similarity index 93%
rename from netbpfload/netbpfload.mainline.rc
rename to netbpfload/netbpfload.35rc
index 0ac5de8..0fbcb5a 100644
--- a/netbpfload/netbpfload.mainline.rc
+++ b/netbpfload/netbpfload.35rc
@@ -2,6 +2,7 @@
     capabilities CHOWN SYS_ADMIN NET_ADMIN
     group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
     user root
+    file /dev/kmsg w
     rlimit memlock 1073741824 1073741824
     oneshot
     reboot_on_failure reboot,bpfloader-failed
diff --git a/netbpfload/netbpfload.rc b/netbpfload/netbpfload.rc
index 14181dc..e1af47f 100644
--- a/netbpfload/netbpfload.rc
+++ b/netbpfload/netbpfload.rc
@@ -17,15 +17,18 @@
 on load_bpf_programs
     exec_start bpfloader
 
-service bpfloader /system/bin/netbpfload
+# Note: This will actually execute /apex/com.android.tethering/bin/netbpfload
+# by virtue of 'service bpfloader' being overridden by the apex shipped .rc
+# Warning: most of the below settings are irrelevant unless the apex is missing.
+service bpfloader /system/bin/false
     # netbpfload will do network bpf loading, then execute /system/bin/bpfloader
-    capabilities CHOWN SYS_ADMIN NET_ADMIN
+    #! capabilities CHOWN SYS_ADMIN NET_ADMIN
     # The following group memberships are a workaround for lack of DAC_OVERRIDE
     # and allow us to open (among other things) files that we created and are
     # no longer root owned (due to CHOWN) but still have group read access to
     # one of the following groups.  This is not perfect, but a more correct
     # solution requires significantly more effort to implement.
-    group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+    #! group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
     user root
     #
     # Set RLIMIT_MEMLOCK to 1GiB for bpfloader
@@ -55,7 +58,7 @@
     #
     # As such we simply use 1GiB as a reasonable approximation of infinity.
     #
-    rlimit memlock 1073741824 1073741824
+    #! rlimit memlock 1073741824 1073741824
     oneshot
     #
     # How to debug bootloops caused by 'bpfloader-failed'.
@@ -81,6 +84,5 @@
     #    'cannot prove return value is 0 or 1' or 'unsupported / unknown operation / helper',
     #    'invalid bpf_context access', etc.
     #
-    reboot_on_failure reboot,bpfloader-failed
-    # we're not really updatable, but want to be able to load bpf programs shipped in apexes
+    reboot_on_failure reboot,netbpfload-missing
     updatable
diff --git a/netd/Android.bp b/netd/Android.bp
index eedbdae..fe4d999 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -72,6 +72,8 @@
         "BpfHandlerTest.cpp",
         "BpfBaseTest.cpp",
     ],
+    version_script: ":connectivity_mainline_test_map",
+    stl: "libc++_static",
     static_libs: [
         "libbase",
         "libnetd_updatable",
diff --git a/netd/BpfBaseTest.cpp b/netd/BpfBaseTest.cpp
index c979a7b..34dfbb4 100644
--- a/netd/BpfBaseTest.cpp
+++ b/netd/BpfBaseTest.cpp
@@ -56,7 +56,7 @@
 
 TEST_F(BpfBasicTest, TestCgroupMounted) {
     std::string cg2_path;
-    ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_CONTROLLER_NAME, &cg2_path));
+    ASSERT_EQ(true, CgroupGetControllerPath(CGROUPV2_HIERARCHY_NAME, &cg2_path));
     ASSERT_EQ(0, access(cg2_path.c_str(), R_OK));
     ASSERT_EQ(0, access((cg2_path + "/cgroup.controllers").c_str(), R_OK));
 }
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index a00c363..9682545 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define LOG_TAG "BpfHandler"
+#define LOG_TAG "NetdUpdatable"
 
 #include "BpfHandler.h"
 
@@ -34,6 +34,7 @@
 namespace net {
 
 using base::unique_fd;
+using base::WaitForProperty;
 using bpf::getSocketCookie;
 using bpf::retrieveProgram;
 using netdutils::Status;
@@ -85,31 +86,6 @@
         return Status("U+ platform with kernel version < 4.14.0 is unsupported");
     }
 
-    if (modules::sdklevel::IsAtLeastV()) {
-        // V bumps the kernel requirement up to 4.19
-        // see also: //system/netd/tests/kernel_test.cpp TestKernel419
-        if (!bpf::isAtLeastKernelVersion(4, 19, 0)) {
-            return Status("V+ platform with kernel version < 4.19.0 is unsupported");
-        }
-
-        // Technically already required by U, but only enforce on V+
-        // see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
-        if (bpf::isKernel32Bit() && bpf::isAtLeastKernelVersion(5, 16, 0)) {
-            return Status("V+ platform with 32 bit kernel, version >= 5.16.0 is unsupported");
-        }
-    }
-
-    // Linux 6.1 is highest version supported by U, starting with V new kernels,
-    // ie. 6.2+ we are dropping various kernel/system userspace 32-on-64 hacks
-    // (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
-    // Note: this check/enforcement only applies to *system* userspace code,
-    // it does not affect unprivileged apps, the 32-on-64 compatibility
-    // problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
-    // see also: //system/bpf/bpfloader/BpfLoader.cpp main()
-    if (bpf::isUserspace32bit() && bpf::isAtLeastKernelVersion(6, 2, 0)) {
-        return Status("32 bit userspace with Kernel version >= 6.2.0 is unsupported");
-    }
-
     // U mandates this mount point (though it should also be the case on T)
     if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) {
         return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
@@ -134,8 +110,35 @@
     // TODO: delete the if statement once all devices should support cgroup
     // socket filter (ie. the minimum kernel version required is 4.14).
     if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
-        RETURN_IF_NOT_OK(
-                attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_CREATE_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
+    }
+
+    if (modules::sdklevel::IsAtLeastV()) {
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT4_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_INET4_CONNECT));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_CONNECT6_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_INET6_CONNECT));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_RECVMSG_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_UDP4_RECVMSG));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_RECVMSG_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_UDP6_RECVMSG));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP4_SENDMSG_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_UDP4_SENDMSG));
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_UDP6_SENDMSG_PROG_PATH,
+                                    cg_fd, BPF_CGROUP_UDP6_SENDMSG));
+
+        if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
+            RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_GETSOCKOPT_PROG_PATH,
+                                        cg_fd, BPF_CGROUP_GETSOCKOPT));
+            RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SETSOCKOPT_PROG_PATH,
+                                        cg_fd, BPF_CGROUP_SETSOCKOPT));
+        }
+
+        if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+            RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_INET_RELEASE_PROG_PATH,
+                                        cg_fd, BPF_CGROUP_INET_SOCK_RELEASE));
+        }
     }
 
     if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
@@ -155,6 +158,24 @@
         if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
     }
 
+    if (modules::sdklevel::IsAtLeastV()) {
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_CONNECT) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_CONNECT) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_RECVMSG) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_RECVMSG) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP4_SENDMSG) <= 0) abort();
+        if (bpf::queryProgram(cg_fd, BPF_CGROUP_UDP6_SENDMSG) <= 0) abort();
+
+        if (bpf::isAtLeastKernelVersion(5, 4, 0)) {
+            if (bpf::queryProgram(cg_fd, BPF_CGROUP_GETSOCKOPT) <= 0) abort();
+            if (bpf::queryProgram(cg_fd, BPF_CGROUP_SETSOCKOPT) <= 0) abort();
+        }
+
+        if (bpf::isAtLeastKernelVersion(5, 10, 0)) {
+            if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_RELEASE) <= 0) abort();
+        }
+    }
+
     return netdutils::status::ok;
 }
 
@@ -165,9 +186,50 @@
 BpfHandler::BpfHandler(uint32_t perUidLimit, uint32_t totalLimit)
     : mPerUidStatsEntriesLimit(perUidLimit), mTotalUidStatsEntriesLimit(totalLimit) {}
 
+static bool mainlineNetBpfLoadDone() {
+    return !access("/sys/fs/bpf/netd_shared/mainline_done", F_OK);
+}
+
+// copied with minor changes from waitForProgsLoaded()
+// p/m/C's staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
+static inline void waitForNetProgsLoaded() {
+    // infinite loop until success with 5/10/20/40/60/60/60... delay
+    for (int delay = 5;; delay *= 2) {
+        if (delay > 60) delay = 60;
+        if (WaitForProperty("init.svc.mdnsd_netbpfload", "stopped", std::chrono::seconds(delay))
+            && mainlineNetBpfLoadDone())
+            return;
+        ALOGW("Waited %ds for init.svc.mdnsd_netbpfload=stopped, still waiting...", delay);
+    }
+}
+
 Status BpfHandler::init(const char* cg2_path) {
-    // Make sure BPF programs are loaded before doing anything
-    android::bpf::waitForProgsLoaded();
+    // Note: netd *can* be restarted, so this might get called a second time after boot is complete
+    // at which point we don't need to (and shouldn't) wait for (more importantly start) loading bpf
+
+    if (base::GetProperty("bpf.progs_loaded", "") != "1") {
+        // AOSP platform netd & mainline don't need this (at least prior to U QPR3),
+        // but there could be platform provided (xt_)bpf programs that oem/vendor
+        // modified netd (which calls us during init) depends on...
+        ALOGI("Waiting for platform BPF programs");
+        android::bpf::waitForProgsLoaded();
+    }
+
+    if (!mainlineNetBpfLoadDone()) {
+        // We're on < U QPR3 & it's the first time netd is starting up (unless crashlooping)
+        //
+        // On U QPR3+ netbpfload is guaranteed to run before the platform bpfloader,
+        // so waitForProgsLoaded() implies mainlineNetBpfLoadDone().
+        if (!base::SetProperty("ctl.start", "mdnsd_netbpfload")) {
+            ALOGE("Failed to set property ctl.start=mdnsd_netbpfload, see dmesg for reason.");
+            abort();
+        }
+
+        ALOGI("Waiting for Networking BPF programs");
+        waitForNetProgsLoaded();
+        ALOGI("Networking BPF programs are loaded");
+    }
+
     ALOGI("BPF programs are loaded");
 
     RETURN_IF_NOT_OK(initPrograms(cg2_path));
@@ -176,7 +238,33 @@
     return netdutils::status::ok;
 }
 
+static void mapLockTest(void) {
+    // The maps must be R/W, and as yet unopened (or more specifically not yet lock'ed).
+    const char * const m1 = BPF_NETD_PATH "map_netd_lock_array_test_map";
+    const char * const m2 = BPF_NETD_PATH "map_netd_lock_hash_test_map";
+
+    unique_fd fd0(bpf::mapRetrieveExclusiveRW(m1)); if (!fd0.ok()) abort();  // grabs exclusive lock
+
+    unique_fd fd1(bpf::mapRetrieveExclusiveRW(m2)); if (!fd1.ok()) abort();  // no conflict with fd0
+    unique_fd fd2(bpf::mapRetrieveExclusiveRW(m2)); if ( fd2.ok()) abort();  // busy due to fd1
+    unique_fd fd3(bpf::mapRetrieveRO(m2));          if (!fd3.ok()) abort();  // no lock taken
+    unique_fd fd4(bpf::mapRetrieveRW(m2));          if ( fd4.ok()) abort();  // busy due to fd1
+    fd1.reset();  // releases exclusive lock
+    unique_fd fd5(bpf::mapRetrieveRO(m2));          if (!fd5.ok()) abort();  // no lock taken
+    unique_fd fd6(bpf::mapRetrieveRW(m2));          if (!fd6.ok()) abort();  // now ok
+    unique_fd fd7(bpf::mapRetrieveRO(m2));          if (!fd7.ok()) abort();  // no lock taken
+    unique_fd fd8(bpf::mapRetrieveExclusiveRW(m2)); if ( fd8.ok()) abort();  // busy due to fd6
+
+    fd0.reset();  // releases exclusive lock
+    unique_fd fd9(bpf::mapRetrieveWO(m1));          if (!fd9.ok()) abort();  // grabs exclusive lock
+}
+
 Status BpfHandler::initMaps() {
+    // bpfLock() requires bpfGetFdMapId which is only available on 4.14+ kernels.
+    if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
+        mapLockTest();
+    }
+
     RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
     RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
     RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
diff --git a/networksecurity/OWNERS b/networksecurity/OWNERS
new file mode 100644
index 0000000..1a4130a
--- /dev/null
+++ b/networksecurity/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1479456
+
+sandrom@google.com
+tweek@google.com
diff --git a/tests/cts/hostside/instrumentation_arguments/Android.bp b/networksecurity/framework/Android.bp
similarity index 71%
rename from tests/cts/hostside/instrumentation_arguments/Android.bp
rename to networksecurity/framework/Android.bp
index cdede36..2b77926 100644
--- a/tests/cts/hostside/instrumentation_arguments/Android.bp
+++ b/networksecurity/framework/Android.bp
@@ -1,3 +1,4 @@
+//
 // Copyright (C) 2024 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
@@ -11,12 +12,20 @@
 // 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_team: "trendy_team_platform_security",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 filegroup {
-    name: "ArgumentConstants",
-    srcs: ["src/**/*.java"],
+    name: "framework-networksecurity-sources",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.aidl",
+    ],
+    path: "src",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
 }
diff --git a/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
new file mode 100644
index 0000000..94521ae
--- /dev/null
+++ b/networksecurity/framework/src/android/net/ct/CertificateTransparencyManager.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.ct;
+
+import android.annotation.FlaggedApi;
+import android.annotation.SystemService;
+
+import com.android.net.ct.flags.Flags;
+
+/**
+ * Provides the primary API for the Certificate Transparency Manager.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CERTIFICATE_TRANSPARENCY_SERVICE)
+@SystemService(CertificateTransparencyManager.SERVICE_NAME)
+public final class CertificateTransparencyManager {
+
+    public static final String SERVICE_NAME = "certificate_transparency";
+
+    /**
+     * Creates a new CertificateTransparencyManager instance.
+     *
+     * @hide
+     */
+    public CertificateTransparencyManager() {}
+}
diff --git a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
similarity index 65%
copy from tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
copy to networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
index 472e347..b5bce7f 100644
--- a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
+++ b/networksecurity/framework/src/android/net/ct/ICertificateTransparencyManager.aidl
@@ -1,11 +1,11 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
+/**
+ * Copyright (c) 2024, The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ *     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,
@@ -13,9 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.net.ct;
 
-package com.android.cts.net.arguments;
-
-public interface InstrumentationArguments {
-    String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
-}
+/**
+* Interface for communicating with CertificateTransparencyService.
+* @hide
+*/
+interface ICertificateTransparencyManager {}
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
new file mode 100644
index 0000000..66d201a
--- /dev/null
+++ b/networksecurity/service/Android.bp
@@ -0,0 +1,40 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package {
+    default_team: "trendy_team_platform_security",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Main lib for Certificate Transparency services.
+java_library {
+    name: "service-networksecurity-pre-jarjar",
+    defaults: ["framework-system-server-module-defaults"],
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    libs: [
+        "framework-connectivity-pre-jarjar",
+        "service-connectivity-pre-jarjar",
+    ],
+
+    // This is included in service-connectivity which is 30+
+    // TODO (b/293613362): allow APEXes to have service jars with higher min_sdk than the APEX
+    // (service-connectivity is only used on 31+) and use 31 here
+    min_sdk_version: "30",
+    sdk_version: "system_server_current",
+    apex_available: ["com.android.tethering"],
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
new file mode 100644
index 0000000..8c53bf7
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.net.ct.ICertificateTransparencyManager;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.net.ct.flags.Flags;
+import com.android.net.module.util.DeviceConfigUtils;
+
+/** Implementation of the Certificate Transparency service. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+
+    private static final String TAG = "CertificateTransparency";
+    private static final String CERTIFICATE_TRANSPARENCY_ENABLED =
+            "certificate_transparency_service_enabled";
+
+    /**
+     * @return true if the CertificateTransparency service is enabled.
+     */
+    public static boolean enabled(Context context) {
+        // TODO: replace isTetheringFeatureEnabled with CT namespace flag.
+        return DeviceConfigUtils.isTetheringFeatureEnabled(
+                        context, CERTIFICATE_TRANSPARENCY_ENABLED)
+                && Flags.certificateTransparencyService();
+    }
+
+    /** Creates a new {@link CertificateTransparencyService} object. */
+    public CertificateTransparencyService(Context context) {}
+
+    /**
+     * Called by {@link com.android.server.ConnectivityServiceInitializer}.
+     *
+     * @see com.android.server.SystemService#onBootPhase
+     */
+    public void onBootPhase(int phase) {
+        Log.d(TAG, "CertificateTransparencyService#onBootPhase " + phase);
+    }
+}
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index c0ac779..fc91e0c 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -12,7 +12,7 @@
     srcs: ["src/lib.rs"],
     rustlibs: [
         "libbinder_rs",
-        "libjni",
+        "libjni_legacy",
         "liblazy_static",
         "liblog_rust",
         "liblogger",
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 779f354..1e1f7e2 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -59,6 +59,7 @@
         "framework-wifi",
         "service-connectivity-pre-jarjar",
         "service-nearby-pre-jarjar",
+        "service-networksecurity-pre-jarjar",
         "service-thread-pre-jarjar",
         service_remoteauth_pre_jarjar_lib,
         "ServiceConnectivityResources",
@@ -100,7 +101,7 @@
     min_sdk_version: "21",
     lint: {
         error_checks: ["NewApi"],
-
+        baseline_filename: "lint-baseline-service-connectivity-mdns-standalone-build-test.xml",
     },
     srcs: [
         "src/com/android/server/connectivity/mdns/**/*.java",
diff --git a/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
new file mode 100644
index 0000000..232d31c
--- /dev/null
+++ b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
@@ -0,0 +1,972 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+            line="37"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+        errorLine1="@FlaggedApi(&quot;com.android.net.flags.register_nsd_offload_engine_api&quot;)"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+            line="43"
+            column="13"/>
+    </issue>
+
+</issues>
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index c620634..b1925bd 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -76,6 +76,8 @@
         "-Wno-unused-parameter",
         "-Wthread-safety",
     ],
+    version_script: ":connectivity_mainline_test_map",
+    stl: "libc++_static",
     static_libs: [
         "libbase",
         "libgmock",
diff --git a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
index 42a922d..a92dfaf 100644
--- a/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
+++ b/service-t/src/com/android/metrics/NetworkNsdReportedMetrics.java
@@ -193,7 +193,8 @@
      * @param sentQueryCount The count of sent queries before stop discovery.
      */
     public void reportServiceDiscoveryStop(boolean isLegacy, int transactionId, long durationMs,
-            int foundCallbackCount, int lostCallbackCount, int servicesCount, int sentQueryCount) {
+            int foundCallbackCount, int lostCallbackCount, int servicesCount, int sentQueryCount,
+            boolean isServiceFromCache) {
         final Builder builder = makeReportedBuilder(isLegacy, transactionId);
         builder.setType(NsdEventType.NET_DISCOVER);
         builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_DISCOVERY_STOP);
@@ -202,6 +203,7 @@
         builder.setLostCallbackCount(lostCallbackCount);
         builder.setFoundServiceCount(servicesCount);
         builder.setSentQueryCount(sentQueryCount);
+        builder.setIsKnownService(isServiceFromCache);
         mDependencies.statsWrite(builder.build());
     }
 
@@ -247,12 +249,15 @@
      * @param isLegacy Whether this call is using legacy backend.
      * @param transactionId The transaction id of service resolution.
      * @param durationMs The duration before stop resolving the service.
+     * @param sentQueryCount The count of sent queries during resolving.
      */
-    public void reportServiceResolutionStop(boolean isLegacy, int transactionId, long durationMs) {
+    public void reportServiceResolutionStop(boolean isLegacy, int transactionId, long durationMs,
+            int sentQueryCount) {
         final Builder builder = makeReportedBuilder(isLegacy, transactionId);
         builder.setType(NsdEventType.NET_RESOLVE);
         builder.setQueryResult(MdnsQueryResult.MQR_SERVICE_RESOLUTION_STOP);
         builder.setEventDurationMillisec(durationMs);
+        builder.setSentQueryCount(sentQueryCount);
         mDependencies.statsWrite(builder.build());
     }
 
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 1ac2f6e..5d23fdc 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -28,6 +28,7 @@
 import com.android.server.ethernet.EthernetService;
 import com.android.server.ethernet.EthernetServiceImpl;
 import com.android.server.nearby.NearbyService;
+import com.android.server.net.ct.CertificateTransparencyService;
 import com.android.server.thread.ThreadNetworkService;
 
 /**
@@ -43,6 +44,7 @@
     private final NearbyService mNearbyService;
     private final EthernetServiceImpl mEthernetServiceImpl;
     private final ThreadNetworkService mThreadNetworkService;
+    private final CertificateTransparencyService mCertificateTransparencyService;
 
     public ConnectivityServiceInitializer(Context context) {
         super(context);
@@ -55,6 +57,7 @@
         mNsdService = createNsdService(context);
         mNearbyService = createNearbyService(context);
         mThreadNetworkService = createThreadNetworkService(context);
+        mCertificateTransparencyService = createCertificateTransparencyService(context);
     }
 
     @Override
@@ -111,6 +114,10 @@
         if (mThreadNetworkService != null) {
             mThreadNetworkService.onBootPhase(phase);
         }
+
+        if (SdkLevel.isAtLeastV() && mCertificateTransparencyService != null) {
+            mCertificateTransparencyService.onBootPhase(phase);
+        }
     }
 
     /**
@@ -186,4 +193,13 @@
         }
         return new ThreadNetworkService(context);
     }
+
+    /** Return CertificateTransparencyService instance if enable, otherwise null. */
+    @Nullable
+    private CertificateTransparencyService createCertificateTransparencyService(
+            final Context context) {
+        return SdkLevel.isAtLeastV() && CertificateTransparencyService.enabled(context)
+                ? new CertificateTransparencyService(context)
+                : null;
+    }
 }
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index ea91e64..54b9ced 100644
--- a/service-t/src/com/android/server/IpSecService.java
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -1877,6 +1877,10 @@
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.ACCESS_NETWORK_STATE, "IpsecService#getTransformState");
 
+        if (transformId == INVALID_RESOURCE_ID) {
+            throw new IllegalStateException("This transform is already closed");
+        }
+
         UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
         TransformRecord transformInfo =
                 userRecord.mTransformRecords.getResourceOrThrow(transformId);
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index cfb1a33..5f672e7 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -93,6 +93,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.InetAddressUtils;
 import com.android.net.module.util.PermissionUtils;
@@ -201,6 +202,7 @@
     private static final int NO_SENT_QUERY_COUNT = 0;
     private static final int DISCOVERY_QUERY_SENT_CALLBACK = 1000;
     private static final int MAX_SUBTYPE_COUNT = 100;
+    private static final int DNSSEC_PROTOCOL = 3;
     private static final SharedLog LOGGER = new SharedLog("serviceDiscovery");
 
     private final Context mContext;
@@ -275,7 +277,7 @@
     }
 
     @VisibleForTesting
-    static class MdnsListener implements MdnsServiceBrowserListener {
+    abstract static class MdnsListener implements MdnsServiceBrowserListener {
         protected final int mClientRequestId;
         protected final int mTransactionId;
         @NonNull
@@ -321,6 +323,10 @@
 
         @Override
         public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) { }
+
+        // Ensure toString gets overridden
+        @NonNull
+        public abstract String toString();
     }
 
     private class DiscoveryListener extends MdnsListener {
@@ -351,13 +357,21 @@
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return String.format("DiscoveryListener: serviceType=%s", getListenedServiceType());
+        }
     }
 
     private class ResolutionListener extends MdnsListener {
+        private final String mServiceName;
 
         ResolutionListener(int clientRequestId, int transactionId,
-                @NonNull String listenServiceType) {
+                @NonNull String listenServiceType, @NonNull String serviceName) {
             super(clientRequestId, transactionId, listenServiceType);
+            mServiceName = serviceName;
         }
 
         @Override
@@ -373,13 +387,22 @@
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return String.format("ResolutionListener serviceName=%s, serviceType=%s",
+                    mServiceName, getListenedServiceType());
+        }
     }
 
     private class ServiceInfoListener extends MdnsListener {
+        private final String mServiceName;
 
         ServiceInfoListener(int clientRequestId, int transactionId,
-                @NonNull String listenServiceType) {
+                @NonNull String listenServiceType, @NonNull String serviceName) {
             super(clientRequestId, transactionId, listenServiceType);
+            this.mServiceName = serviceName;
         }
 
         @Override
@@ -410,6 +433,13 @@
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return String.format("ServiceInfoListener serviceName=%s, serviceType=%s",
+                    mServiceName, getListenedServiceType());
+        }
     }
 
     private class SocketRequestMonitor implements MdnsSocketProvider.SocketRequestMonitor {
@@ -656,9 +686,12 @@
             }
 
             private void storeAdvertiserRequestMap(int clientRequestId, int transactionId,
-                    ClientInfo clientInfo, @Nullable Network requestedNetwork) {
+                    ClientInfo clientInfo, @NonNull NsdServiceInfo serviceInfo) {
+                final String serviceFullName =
+                        serviceInfo.getServiceName() + "." + serviceInfo.getServiceType();
                 clientInfo.mClientRequests.put(clientRequestId, new AdvertiserClientRequest(
-                        transactionId, requestedNetwork, mClock.elapsedRealtime()));
+                        transactionId, serviceInfo.getNetwork(), serviceFullName,
+                        mClock.elapsedRealtime()));
                 mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
                 updateMulticastLock();
             }
@@ -736,7 +769,7 @@
             private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
                 final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
                 for (String subtype : subtypes) {
-                    subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
+                    subtypeMap.put(DnsUtils.toDnsUpperCase(subtype), subtype);
                 }
                 return new ArraySet<>(subtypeMap.values());
             }
@@ -978,6 +1011,17 @@
                                 break;
                             }
 
+                            if (!checkPublicKey(serviceInfo.getPublicKey())) {
+                                Log.e(TAG,
+                                        "Invalid public key: "
+                                                + Arrays.toString(serviceInfo.getPublicKey()));
+                                clientInfo.onRegisterServiceFailedImmediately(
+                                        clientRequestId,
+                                        NsdManager.FAILURE_BAD_PARAMETERS,
+                                        false /* isLegacy */);
+                                break;
+                            }
+
                             Set<String> subtypes = new ArraySet<>(serviceInfo.getSubtypes());
                             if (typeSubtype != null && typeSubtype.second != null) {
                                 for (String subType : typeSubtype.second) {
@@ -1010,7 +1054,7 @@
                             mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
                                     mdnsAdvertisingOptions, clientInfo.mUid);
                             storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
-                                    serviceInfo.getNetwork());
+                                    serviceInfo);
                         } else {
                             maybeStartDaemon();
                             transactionId = getUniqueId();
@@ -1104,9 +1148,12 @@
 
                             maybeStartMonitoringSockets();
                             final MdnsListener listener = new ResolutionListener(clientRequestId,
-                                    transactionId, resolveServiceType);
+                                    transactionId, resolveServiceType, info.getServiceName());
+                            final int ifaceIdx = info.getNetwork() != null
+                                    ? 0 : info.getInterfaceIndex();
                             final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                     .setNetwork(info.getNetwork())
+                                    .setInterfaceIndex(ifaceIdx)
                                     .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
                                             ? AGGRESSIVE_QUERY_MODE
                                             : PASSIVE_QUERY_MODE)
@@ -1204,9 +1251,12 @@
 
                         maybeStartMonitoringSockets();
                         final MdnsListener listener = new ServiceInfoListener(clientRequestId,
-                                transactionId, resolveServiceType);
+                                transactionId, resolveServiceType, info.getServiceName());
+                        final int ifIndex = info.getNetwork() != null
+                                ? 0 : info.getInterfaceIndex();
                         final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                 .setNetwork(info.getNetwork())
+                                .setInterfaceIndex(ifIndex)
                                 .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
                                         ? AGGRESSIVE_QUERY_MODE
                                         : PASSIVE_QUERY_MODE)
@@ -1585,6 +1635,12 @@
                         NsdManager.nameOf(code), transactionId));
                 switch (code) {
                     case NsdManager.SERVICE_FOUND:
+                        // Set the ServiceFromCache flag only if the service is actually being
+                        // retrieved from the cache. This flag should not be overridden by later
+                        // service found event, which may not be cached.
+                        if (event.mIsServiceFromCache) {
+                            request.setServiceFromCache(true);
+                        }
                         clientInfo.onServiceFound(clientRequestId, info, request);
                         break;
                     case NsdManager.SERVICE_LOST:
@@ -1788,15 +1844,42 @@
      * <p>For now NsdService only allows single-label hostnames conforming to RFC 1035. In other
      * words, the hostname should be at most 63 characters long and it only contains letters, digits
      * and hyphens.
+     *
+     * <p>Additionally, this allows hostname starting with a digit to support Matter devices. Per
+     * Matter spec 4.3.1.1:
+     *
+     * <p>The target host name SHALL be constructed using one of the available link-layer addresses,
+     * such as a 48-bit device MAC address (for Ethernet and Wi‑Fi) or a 64-bit MAC Extended Address
+     * (for Thread) expressed as a fixed-length twelve-character (or sixteen-character) hexadecimal
+     * string, encoded as ASCII (UTF-8) text using capital letters, e.g., B75AFB458ECD.<domain>.
      */
     public static boolean checkHostname(@Nullable String hostname) {
         if (hostname == null) {
             return true;
         }
-        String HOSTNAME_REGEX = "^[a-zA-Z]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$";
+        String HOSTNAME_REGEX = "^[a-zA-Z0-9]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$";
         return Pattern.compile(HOSTNAME_REGEX).matcher(hostname).matches();
     }
 
+    /**
+     * Checks if the public key is valid.
+     *
+     * <p>For simplicity, it only checks if the protocol is DNSSEC and the RDATA is not fewer than 4
+     * bytes. See RFC 3445 Section 3.
+     *
+     * <p>Message format: flags (2 bytes), protocol (1 byte), algorithm (1 byte), public key.
+     */
+    private static boolean checkPublicKey(@Nullable byte[] publicKey) {
+        if (publicKey == null) {
+            return true;
+        }
+        if (publicKey.length < 4) {
+            return false;
+        }
+        int protocol = publicKey[2];
+        return protocol == DNSSEC_PROTOCOL;
+    }
+
     /** Returns {@code true} if {@code subtype} is a valid DNS-SD subtype label. */
     private static boolean checkSubtypeLabel(String subtype) {
         return Pattern.compile("^" + SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
@@ -1841,16 +1924,20 @@
                         mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
                 .setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
                         mContext, MdnsFeatureFlags.INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING))
-                .setIsExpiredServicesRemovalEnabled(mDeps.isFeatureEnabled(
+                .setIsExpiredServicesRemovalEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
                 .setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
-                .setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
+                .setIsKnownAnswerSuppressionEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
-                .setIsUnicastReplyEnabled(mDeps.isFeatureEnabled(
+                .setIsUnicastReplyEnabled(mDeps.isTetheringFeatureNotChickenedOut(
                         mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED))
                 .setIsAggressiveQueryModeEnabled(mDeps.isFeatureEnabled(
                         mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
+                .setIsQueryWithKnownAnswerEnabled(mDeps.isFeatureEnabled(
+                        mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
+                .setAvoidAdvertisingEmptyTxtRecords(mDeps.isTetheringFeatureNotChickenedOut(
+                        mContext, MdnsFeatureFlags.NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS))
                 .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
                         mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
                 .build();
@@ -2543,6 +2630,17 @@
         // Dump state machine logs
         mNsdStateMachine.dump(fd, pw, args);
 
+        // Dump clients
+        pw.println();
+        pw.println("Active clients:");
+        pw.increaseIndent();
+        HandlerUtils.runWithScissorsForDump(mNsdStateMachine.getHandler(), () -> {
+            for (ClientInfo clientInfo : mClients.values()) {
+                pw.println(clientInfo.toString());
+            }
+        }, 10_000);
+        pw.decreaseIndent();
+
         // Dump service and clients logs
         pw.println();
         pw.println("Logs:");
@@ -2615,6 +2713,21 @@
         public int getSentQueryCount() {
             return mSentQueryCount;
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return getRequestDescriptor() + " {" + mTransactionId
+                    + ", startTime " + mStartTimeMs
+                    + ", foundServices " + mFoundServiceCount
+                    + ", lostServices " + mLostServiceCount
+                    + ", fromCache " + mIsServiceFromCache
+                    + ", sentQueries " + mSentQueryCount
+                    + "}";
+        }
+
+        @NonNull
+        protected abstract String getRequestDescriptor();
     }
 
     private static class LegacyClientRequest extends ClientRequest {
@@ -2624,6 +2737,12 @@
             super(transactionId, startTimeMs);
             mRequestCode = requestCode;
         }
+
+        @NonNull
+        @Override
+        protected String getRequestDescriptor() {
+            return "Legacy (" + mRequestCode + ")";
+        }
     }
 
     private abstract static class JavaBackendClientRequest extends ClientRequest {
@@ -2643,9 +2762,20 @@
     }
 
     private static class AdvertiserClientRequest extends JavaBackendClientRequest {
+        @NonNull
+        private final String mServiceFullName;
+
         private AdvertiserClientRequest(int transactionId, @Nullable Network requestedNetwork,
-                long startTimeMs) {
+                @NonNull String serviceFullName, long startTimeMs) {
             super(transactionId, requestedNetwork, startTimeMs);
+            mServiceFullName = serviceFullName;
+        }
+
+        @NonNull
+        @Override
+        public String getRequestDescriptor() {
+            return String.format("Advertiser: serviceFullName=%s, net=%s",
+                    mServiceFullName, getRequestedNetwork());
         }
     }
 
@@ -2658,6 +2788,12 @@
             super(transactionId, requestedNetwork, startTimeMs);
             mListener = listener;
         }
+
+        @NonNull
+        @Override
+        public String getRequestDescriptor() {
+            return String.format("Discovery/%s, net=%s", mListener, getRequestedNetwork());
+        }
     }
 
     /* Information tracked per client */
@@ -2702,17 +2838,15 @@
         @Override
         public String toString() {
             StringBuilder sb = new StringBuilder();
-            sb.append("mResolvedService ").append(mResolvedService).append("\n");
-            sb.append("mIsLegacy ").append(mIsPreSClient).append("\n");
-            sb.append("mUseJavaBackend ").append(mUseJavaBackend).append("\n");
-            sb.append("mUid ").append(mUid).append("\n");
+            sb.append("mUid ").append(mUid).append(", ");
+            sb.append("mResolvedService ").append(mResolvedService).append(", ");
+            sb.append("mIsLegacy ").append(mIsPreSClient).append(", ");
+            sb.append("mUseJavaBackend ").append(mUseJavaBackend).append(", ");
+            sb.append("mClientRequests:\n");
             for (int i = 0; i < mClientRequests.size(); i++) {
                 int clientRequestId = mClientRequests.keyAt(i);
-                sb.append("clientRequestId ")
-                        .append(clientRequestId)
-                        .append(" transactionId ").append(mClientRequests.valueAt(i).mTransactionId)
-                        .append(" type ").append(
-                                mClientRequests.valueAt(i).getClass().getSimpleName())
+                sb.append("  ").append(clientRequestId)
+                        .append(": ").append(mClientRequests.valueAt(i).toString())
                         .append("\n");
             }
             return sb.toString();
@@ -2762,10 +2896,12 @@
                                 request.getFoundServiceCount(),
                                 request.getLostServiceCount(),
                                 request.getServicesCount(),
-                                request.getSentQueryCount());
+                                request.getSentQueryCount(),
+                                request.isServiceFromCache());
                     } else if (listener instanceof ResolutionListener) {
                         mMetrics.reportServiceResolutionStop(false /* isLegacy */, transactionId,
-                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                request.getSentQueryCount());
                     } else if (listener instanceof ServiceInfoListener) {
                         mMetrics.reportServiceInfoCallbackUnregistered(transactionId,
                                 request.calculateRequestDurationMs(mClock.elapsedRealtime()),
@@ -2801,12 +2937,14 @@
                                 request.getFoundServiceCount(),
                                 request.getLostServiceCount(),
                                 request.getServicesCount(),
-                                NO_SENT_QUERY_COUNT);
+                                NO_SENT_QUERY_COUNT,
+                                request.isServiceFromCache());
                         break;
                     case NsdManager.RESOLVE_SERVICE:
                         stopResolveService(transactionId);
                         mMetrics.reportServiceResolutionStop(true /* isLegacy */, transactionId,
-                                request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                                request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                                NO_SENT_QUERY_COUNT);
                         break;
                     case NsdManager.REGISTER_SERVICE:
                         unregisterService(transactionId);
@@ -2923,7 +3061,8 @@
                     request.getFoundServiceCount(),
                     request.getLostServiceCount(),
                     request.getServicesCount(),
-                    request.getSentQueryCount());
+                    request.getSentQueryCount(),
+                    request.isServiceFromCache());
             try {
                 mCb.onStopDiscoverySucceeded(listenerKey);
             } catch (RemoteException e) {
@@ -3022,7 +3161,8 @@
             mMetrics.reportServiceResolutionStop(
                     isLegacyClientRequest(request),
                     request.mTransactionId,
-                    request.calculateRequestDurationMs(mClock.elapsedRealtime()));
+                    request.calculateRequestDurationMs(mClock.elapsedRealtime()),
+                    request.getSentQueryCount());
             try {
                 mCb.onStopResolutionSucceeded(listenerKey);
             } catch (RemoteException e) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index c4d3338..f55db93 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -23,6 +23,8 @@
 import android.text.TextUtils;
 import android.util.Pair;
 
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -63,8 +65,6 @@
     @NonNull
     private final WeakReference<MdnsSocketClientBase> weakRequestSender;
     @NonNull
-    private final MdnsPacketWriter packetWriter;
-    @NonNull
     private final String[] serviceTypeLabels;
     @NonNull
     private final List<String> subtypes;
@@ -79,11 +79,16 @@
     private final MdnsUtils.Clock clock;
     @NonNull
     private final SharedLog sharedLog;
+    @NonNull
+    private final MdnsServiceTypeClient.Dependencies dependencies;
     private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
+    private final byte[] packetCreationBuffer = new byte[1500]; // TODO: use interface MTU
+    @NonNull
+    private final List<MdnsResponse> existingServices;
+    private final boolean isQueryWithKnownAnswer;
 
     EnqueueMdnsQueryCallable(
             @NonNull MdnsSocketClientBase requestSender,
-            @NonNull MdnsPacketWriter packetWriter,
             @NonNull String serviceType,
             @NonNull Collection<String> subtypes,
             boolean expectUnicastResponse,
@@ -93,9 +98,11 @@
             boolean sendDiscoveryQueries,
             @NonNull Collection<MdnsResponse> servicesToResolve,
             @NonNull MdnsUtils.Clock clock,
-            @NonNull SharedLog sharedLog) {
+            @NonNull SharedLog sharedLog,
+            @NonNull MdnsServiceTypeClient.Dependencies dependencies,
+            @NonNull Collection<MdnsResponse> existingServices,
+            boolean isQueryWithKnownAnswer) {
         weakRequestSender = new WeakReference<>(requestSender);
-        this.packetWriter = packetWriter;
         serviceTypeLabels = TextUtils.split(serviceType, "\\.");
         this.subtypes = new ArrayList<>(subtypes);
         this.expectUnicastResponse = expectUnicastResponse;
@@ -106,6 +113,9 @@
         this.servicesToResolve = new ArrayList<>(servicesToResolve);
         this.clock = clock;
         this.sharedLog = sharedLog;
+        this.dependencies = dependencies;
+        this.existingServices = new ArrayList<>(existingServices);
+        this.isQueryWithKnownAnswer = isQueryWithKnownAnswer;
     }
 
     /**
@@ -176,62 +186,86 @@
                 return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
             }
 
+            // Put the existing ptr records into known-answer section.
+            final List<MdnsRecord> knownAnswers = new ArrayList<>();
+            if (sendDiscoveryQueries) {
+                for (MdnsResponse existingService : existingServices) {
+                    for (MdnsPointerRecord ptrRecord : existingService.getPointerRecords()) {
+                        // Ignore any PTR records that don't match the current query.
+                        if (!CollectionUtils.any(questions,
+                                q -> q instanceof MdnsPointerRecord
+                                        && DnsUtils.equalsDnsLabelIgnoreDnsCase(
+                                                q.getName(), ptrRecord.getName()))) {
+                            continue;
+                        }
+
+                        knownAnswers.add(new MdnsPointerRecord(
+                                ptrRecord.getName(),
+                                ptrRecord.getReceiptTime(),
+                                ptrRecord.getCacheFlush(),
+                                ptrRecord.getRemainingTTL(now), // Put the remaining ttl.
+                                ptrRecord.getPointer()));
+                    }
+                }
+            }
+
             final MdnsPacket queryPacket = new MdnsPacket(
                     transactionId,
                     MdnsConstants.FLAGS_QUERY,
                     questions,
-                    Collections.emptyList(), /* answers */
+                    knownAnswers,
                     Collections.emptyList(), /* authorityRecords */
                     Collections.emptyList() /* additionalRecords */);
-            MdnsUtils.writeMdnsPacket(packetWriter, queryPacket);
-            sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT);
+            sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT, queryPacket);
             for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
-                sendPacketToIpv4AndIpv6(requestSender, emulatorPort);
+                sendPacketToIpv4AndIpv6(requestSender, emulatorPort, queryPacket);
             }
             return Pair.create(transactionId, subtypes);
-        } catch (IOException e) {
+        } catch (Exception e) {
             sharedLog.e(String.format("Failed to create mDNS packet for subtype: %s.",
                     TextUtils.join(",", subtypes)), e);
             return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
         }
     }
 
-    private void sendPacket(MdnsSocketClientBase requestSender, InetSocketAddress address)
-            throws IOException {
-        DatagramPacket packet = packetWriter.getPacket(address);
+    private void sendPacket(MdnsSocketClientBase requestSender, InetSocketAddress address,
+            MdnsPacket mdnsPacket) throws IOException {
+        final List<DatagramPacket> packets = dependencies.getDatagramPacketsFromMdnsPacket(
+                packetCreationBuffer, mdnsPacket, address, isQueryWithKnownAnswer);
         if (expectUnicastResponse) {
             // MdnsMultinetworkSocketClient is only available on T+
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
                     && requestSender instanceof MdnsMultinetworkSocketClient) {
                 ((MdnsMultinetworkSocketClient) requestSender).sendPacketRequestingUnicastResponse(
-                        packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
+                        packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
             } else {
                 requestSender.sendPacketRequestingUnicastResponse(
-                        packet, onlyUseIpv6OnIpv6OnlyNetworks);
+                        packets, onlyUseIpv6OnIpv6OnlyNetworks);
             }
         } else {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
                     && requestSender instanceof MdnsMultinetworkSocketClient) {
                 ((MdnsMultinetworkSocketClient) requestSender)
                         .sendPacketRequestingMulticastResponse(
-                                packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
+                                packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
             } else {
                 requestSender.sendPacketRequestingMulticastResponse(
-                        packet, onlyUseIpv6OnIpv6OnlyNetworks);
+                        packets, onlyUseIpv6OnIpv6OnlyNetworks);
             }
         }
     }
 
-    private void sendPacketToIpv4AndIpv6(MdnsSocketClientBase requestSender, int port) {
+    private void sendPacketToIpv4AndIpv6(MdnsSocketClientBase requestSender, int port,
+            MdnsPacket mdnsPacket) {
         try {
             sendPacket(requestSender,
-                    new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port));
+                    new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port), mdnsPacket);
         } catch (IOException e) {
             sharedLog.e("Can't send packet to IPv4", e);
         }
         try {
             sendPacket(requestSender,
-                    new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port));
+                    new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port), mdnsPacket);
         } catch (IOException e) {
             sharedLog.e("Can't send packet to IPv6", e);
         }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index c162bcc..9c52eca 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -41,6 +41,7 @@
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -241,13 +242,10 @@
         }
 
         @Override
-        public void onDestroyed(@NonNull MdnsInterfaceSocket socket) {
-            for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
-                if (mAdvertiserRequests.valueAt(i).onAdvertiserDestroyed(socket)) {
-                    mAdvertiserRequests.removeAt(i);
-                }
-            }
-            mAllAdvertisers.remove(socket);
+        public void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket) {
+            if (DBG) { mSharedLog.i("onAllServicesRemoved: " + socket); }
+            // Try destroying the advertiser if all services has been removed
+            destroyAdvertiser(socket, false /* interfaceDestroyed */);
         }
     };
 
@@ -275,7 +273,7 @@
             return true;
         }
         // Check if it conflicts with the default hostname.
-        return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
+        return DnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
     }
 
     private void updateRegistrationUntilNoConflict(
@@ -318,6 +316,30 @@
     }
 
     /**
+     * Destroys the advertiser for the interface indicated by {@code socket}.
+     *
+     * {@code interfaceDestroyed} should be set to {@code true} if this method is called because
+     * the associated interface has been destroyed.
+     */
+    private void destroyAdvertiser(MdnsInterfaceSocket socket, boolean interfaceDestroyed) {
+        InterfaceAdvertiserRequest advertiserRequest;
+
+        MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.remove(socket);
+        if (advertiser != null) {
+            advertiser.destroyNow();
+            if (DBG) { mSharedLog.i("MdnsInterfaceAdvertiser is destroyed: " + advertiser); }
+        }
+
+        for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
+            advertiserRequest = mAdvertiserRequests.valueAt(i);
+            if (advertiserRequest.onAdvertiserDestroyed(socket, interfaceDestroyed)) {
+                if (DBG) { mSharedLog.i("AdvertiserRequest is removed: " + advertiserRequest); }
+                mAdvertiserRequests.removeAt(i);
+            }
+        }
+    }
+
+    /**
      * A request for a {@link MdnsInterfaceAdvertiser}.
      *
      * This class tracks services to be advertised on all sockets provided via a registered
@@ -336,13 +358,22 @@
         }
 
         /**
-         * Called when an advertiser was destroyed, after all services were unregistered and it sent
-         * exit announcements, or the interface is gone.
+         * Called when the interface advertiser associated with {@code socket} has been destroyed.
          *
-         * @return true if this {@link InterfaceAdvertiserRequest} should now be deleted.
+         * {@code interfaceDestroyed} should be set to {@code true} if this method is called because
+         * the associated interface has been destroyed.
+         *
+         * @return true if the {@link InterfaceAdvertiserRequest} should now be deleted
          */
-        boolean onAdvertiserDestroyed(@NonNull MdnsInterfaceSocket socket) {
+        boolean onAdvertiserDestroyed(
+                @NonNull MdnsInterfaceSocket socket, boolean interfaceDestroyed) {
             final MdnsInterfaceAdvertiser removedAdvertiser = mAdvertisers.remove(socket);
+            if (removedAdvertiser != null
+                    && !interfaceDestroyed && mPendingRegistrations.size() > 0) {
+                mSharedLog.wtf(
+                        "unexpected onAdvertiserDestroyed() when there are pending registrations");
+            }
+
             if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled && removedAdvertiser != null) {
                 final String interfaceName = removedAdvertiser.getSocketInterfaceName();
                 // If the interface is destroyed, stop all hardware offloading on that
@@ -406,8 +437,8 @@
                     continue;
                 }
                 final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
-                if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
-                        && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
+                if (DnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
+                        && DnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
                         other.getServiceType())) {
                     return mPendingRegistrations.keyAt(i);
                 }
@@ -418,10 +449,11 @@
         /**
          * Get the ID of a conflicting registration due to host, or -1 if none.
          *
-         * <p>It's valid that multiple registrations from the same user are using the same hostname.
-         *
          * <p>If there's already another registration with the same hostname requested by another
-         * user, this is considered a conflict.
+         * UID, this is a conflict.
+         *
+         * <p>If there're two registrations both containing address records using the same hostname,
+         * this is a conflict.
          */
         int getConflictingRegistrationDueToHost(@NonNull NsdServiceInfo info, int clientUid) {
             if (TextUtils.isEmpty(info.getHostname())) {
@@ -430,10 +462,17 @@
             for (int i = 0; i < mPendingRegistrations.size(); i++) {
                 final Registration otherRegistration = mPendingRegistrations.valueAt(i);
                 final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
+                final int otherServiceId = mPendingRegistrations.keyAt(i);
                 if (clientUid != otherRegistration.mClientUid
-                        && MdnsUtils.equalsIgnoreDnsCase(
+                        && DnsUtils.equalsIgnoreDnsCase(
                                 info.getHostname(), otherInfo.getHostname())) {
-                    return mPendingRegistrations.keyAt(i);
+                    return otherServiceId;
+                }
+                if (!info.getHostAddresses().isEmpty()
+                        && !otherInfo.getHostAddresses().isEmpty()
+                        && DnsUtils.equalsIgnoreDnsCase(
+                                info.getHostname(), otherInfo.getHostname())) {
+                    return otherServiceId;
                 }
             }
             return -1;
@@ -528,7 +567,7 @@
         public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket) {
             final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
-            if (advertiser != null) advertiser.destroyNow();
+            if (advertiser != null) destroyAdvertiser(socket, true /* interfaceDestroyed */);
         }
 
         @Override
@@ -811,7 +850,7 @@
                 sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
                 continue;
             }
-            priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
+            priorities.put(DnsUtils.toDnsUpperCase(priorityAndType[1]), priority);
         }
         return priorities;
     }
@@ -957,7 +996,7 @@
             @NonNull Registration registration, byte[] rawOffloadPacket) {
         final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
         final Integer mapPriority = mServiceTypeToOffloadPriority.get(
-                MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
+                DnsUtils.toDnsUpperCase(nsdServiceInfo.getServiceType()));
         // Higher values of priority are less prioritized
         final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
         final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 21b7069..8123d27 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -30,6 +30,7 @@
 import androidx.annotation.GuardedBy;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -66,8 +67,8 @@
 
         public void put(@NonNull String serviceType, @NonNull SocketKey socketKey,
                 @NonNull MdnsServiceTypeClient client) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
-            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
                     socketKey);
             clients.put(perSocketServiceType, client);
         }
@@ -75,18 +76,18 @@
         @Nullable
         public MdnsServiceTypeClient get(
                 @NonNull String serviceType, @NonNull SocketKey socketKey) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
-            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsLowerServiceType,
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
+            final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
                     socketKey);
             return clients.getOrDefault(perSocketServiceType, null);
         }
 
         public List<MdnsServiceTypeClient> getByServiceType(@NonNull String serviceType) {
-            final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+            final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
             final List<MdnsServiceTypeClient> list = new ArrayList<>();
             for (int i = 0; i < clients.size(); i++) {
                 final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
-                if (dnsLowerServiceType.equals(perSocketServiceType.first)) {
+                if (dnsUpperServiceType.equals(perSocketServiceType.first)) {
                     list.add(clients.valueAt(i));
                 }
             }
@@ -241,11 +242,30 @@
             }
         }
         // Request the network for discovery.
+        // This requests sockets on all networks even if the searchOptions have a given interface
+        // index (with getNetwork==null, for local interfaces), and only uses matching interfaces
+        // in that case. While this is a simple solution to only use matching sockets, a better
+        // practice would be to only request the correct socket for discovery.
+        // TODO: avoid requesting extra sockets after migrating P2P and tethering networks to local
+        // NetworkAgents.
         socketClient.notifyNetworkRequested(listener, searchOptions.getNetwork(),
                 new MdnsSocketClientBase.SocketCreationCallback() {
                     @Override
                     public void onSocketCreated(@NonNull SocketKey socketKey) {
                         discoveryExecutor.ensureRunningOnHandlerThread();
+                        final int searchInterfaceIndex = searchOptions.getInterfaceIndex();
+                        if (searchOptions.getNetwork() == null
+                                && searchInterfaceIndex > 0
+                                // The interface index in options should only match interfaces that
+                                // do not have any Network; a matching Network should be provided
+                                // otherwise.
+                                && (socketKey.getNetwork() != null
+                                    || socketKey.getInterfaceIndex() != searchInterfaceIndex)) {
+                            sharedLog.i("Skipping " + socketKey + " as ifIndex "
+                                    + searchInterfaceIndex + " was requested.");
+                            return;
+                        }
+
                         // All listeners of the same service types shares the same
                         // MdnsServiceTypeClient.
                         MdnsServiceTypeClient serviceTypeClient =
@@ -362,7 +382,7 @@
         return new MdnsServiceTypeClient(
                 serviceType, socketClient,
                 executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
-                sharedLog.forSubComponent(tag), looper, serviceCache);
+                sharedLog.forSubComponent(tag), looper, serviceCache, mdnsFeatureFlags);
     }
 
     /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 56202fd..709dc79 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -62,6 +62,17 @@
      */
     public static final String NSD_AGGRESSIVE_QUERY_MODE = "nsd_aggressive_query_mode";
 
+    /**
+     * A feature flag to control whether the query with known-answer should be enabled.
+     */
+    public static final String NSD_QUERY_WITH_KNOWN_ANSWER = "nsd_query_with_known_answer";
+
+    /**
+     * A feature flag to avoid advertising empty TXT records, as per RFC 6763 6.1.
+     */
+    public static final String NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS =
+            "nsd_avoid_advertising_empty_txt_records";
+
     // Flag for offload feature
     public final boolean mIsMdnsOffloadFeatureEnabled;
 
@@ -83,6 +94,12 @@
     // Flag for aggressive query mode
     public final boolean mIsAggressiveQueryModeEnabled;
 
+    // Flag for query with known-answer
+    public final boolean mIsQueryWithKnownAnswerEnabled;
+
+    // Flag for avoiding advertising empty TXT records
+    public final boolean mAvoidAdvertisingEmptyTxtRecords;
+
     @Nullable
     private final FlagOverrideProvider mOverrideProvider;
 
@@ -126,6 +143,23 @@
     }
 
     /**
+     * Indicates whether {@link #NSD_QUERY_WITH_KNOWN_ANSWER} is enabled, including for testing.
+     */
+    public boolean isQueryWithKnownAnswerEnabled() {
+        return mIsQueryWithKnownAnswerEnabled
+                || isForceEnabledForTest(NSD_QUERY_WITH_KNOWN_ANSWER);
+    }
+
+    /**
+     * Indicates whether {@link #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS} is enabled, including for
+     * testing.
+     */
+    public boolean avoidAdvertisingEmptyTxtRecords() {
+        return mAvoidAdvertisingEmptyTxtRecords
+                || isForceEnabledForTest(NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS);
+    }
+
+    /**
      * The constructor for {@link MdnsFeatureFlags}.
      */
     public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -135,6 +169,8 @@
             boolean isKnownAnswerSuppressionEnabled,
             boolean isUnicastReplyEnabled,
             boolean isAggressiveQueryModeEnabled,
+            boolean isQueryWithKnownAnswerEnabled,
+            boolean avoidAdvertisingEmptyTxtRecords,
             @Nullable FlagOverrideProvider overrideProvider) {
         mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
         mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -143,6 +179,8 @@
         mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
         mIsUnicastReplyEnabled = isUnicastReplyEnabled;
         mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+        mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+        mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
         mOverrideProvider = overrideProvider;
     }
 
@@ -162,6 +200,8 @@
         private boolean mIsKnownAnswerSuppressionEnabled;
         private boolean mIsUnicastReplyEnabled;
         private boolean mIsAggressiveQueryModeEnabled;
+        private boolean mIsQueryWithKnownAnswerEnabled;
+        private boolean mAvoidAdvertisingEmptyTxtRecords;
         private FlagOverrideProvider mOverrideProvider;
 
         /**
@@ -170,11 +210,13 @@
         public Builder() {
             mIsMdnsOffloadFeatureEnabled = false;
             mIncludeInetAddressRecordsInProbing = false;
-            mIsExpiredServicesRemovalEnabled = false;
+            mIsExpiredServicesRemovalEnabled = true; // Default enabled.
             mIsLabelCountLimitEnabled = true; // Default enabled.
-            mIsKnownAnswerSuppressionEnabled = false;
-            mIsUnicastReplyEnabled = true;
+            mIsKnownAnswerSuppressionEnabled = true; // Default enabled.
+            mIsUnicastReplyEnabled = true; // Default enabled.
             mIsAggressiveQueryModeEnabled = false;
+            mIsQueryWithKnownAnswerEnabled = false;
+            mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
             mOverrideProvider = null;
         }
 
@@ -261,6 +303,26 @@
         }
 
         /**
+         * Set whether the query with known-answer is enabled.
+         *
+         * @see #NSD_QUERY_WITH_KNOWN_ANSWER
+         */
+        public Builder setIsQueryWithKnownAnswerEnabled(boolean isQueryWithKnownAnswerEnabled) {
+            mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+            return this;
+        }
+
+        /**
+         * Set whether to avoid advertising empty TXT records.
+         *
+         * @see #NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS
+         */
+        public Builder setAvoidAdvertisingEmptyTxtRecords(boolean avoidAdvertisingEmptyTxtRecords) {
+            mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
+            return this;
+        }
+
+        /**
          * Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
          */
         public MdnsFeatureFlags build() {
@@ -271,6 +333,8 @@
                     mIsKnownAnswerSuppressionEnabled,
                     mIsUnicastReplyEnabled,
                     mIsAggressiveQueryModeEnabled,
+                    mIsQueryWithKnownAnswerEnabled,
+                    mAvoidAdvertisingEmptyTxtRecords,
                     mOverrideProvider);
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index c2363c0..0b2003f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -22,12 +22,10 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresApi;
 import android.net.LinkAddress;
-import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.HexDump;
@@ -38,6 +36,7 @@
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -102,12 +101,15 @@
                 @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId, int conflictType);
 
         /**
-         * Called by the advertiser when it destroyed itself.
+         * Called when all services on this interface advertiser has already been removed and exit
+         * announcements have been sent.
          *
-         * This can happen after a call to {@link #destroyNow()}, or after all services were
-         * unregistered and the advertiser finished sending exit announcements.
+         * <p>It's guaranteed that there are no service registrations in the
+         * MdnsInterfaceAdvertiser when this callback is invoked.
+         *
+         * <p>This is typically listened by the {@link MdnsAdvertiser} to release the resources
          */
-        void onDestroyed(@NonNull MdnsInterfaceSocket socket);
+        void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket);
     }
 
     /**
@@ -133,6 +135,15 @@
 
             mAnnouncer.startSending(info.getServiceId(), announcementInfo,
                     0L /* initialDelayMs */);
+
+            // Re-announce the services which have the same custom hostname.
+            final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId());
+            if (hostname != null) {
+                final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
+                        new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname));
+                announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId());
+                reannounceServices(announcementInfos);
+            }
         }
     }
 
@@ -149,10 +160,11 @@
         public void onFinished(@NonNull BaseAnnouncementInfo info) {
             if (info instanceof MdnsAnnouncer.ExitAnnouncementInfo) {
                 mRecordRepository.removeService(info.getServiceId());
-
-                if (mRecordRepository.getServicesCount() == 0) {
-                    destroyNow();
-                }
+                mCbHandler.post(() -> {
+                    if (mRecordRepository.getServicesCount() == 0) {
+                        mCb.onAllServicesRemoved(mSocket);
+                    }
+                });
             }
         }
     }
@@ -234,8 +246,7 @@
      * Start the advertiser.
      *
      * The advertiser will stop itself when all services are removed and exit announcements sent,
-     * notifying via {@link Callback#onDestroyed}. This can also be triggered manually via
-     * {@link #destroyNow()}.
+     * notifying via {@link Callback#onAllServicesRemoved}.
      */
     public void start() {
         mSocket.addPacketHandler(this);
@@ -281,10 +292,11 @@
         if (!mRecordRepository.hasActiveService(id)) return;
         mProber.stop(id);
         mAnnouncer.stop(id);
+        final String hostname = mRecordRepository.getHostnameForServiceId(id);
         final MdnsAnnouncer.ExitAnnouncementInfo exitInfo = mRecordRepository.exitService(id);
         if (exitInfo != null) {
-            // This effectively schedules destroyNow(), as it is to be called when the exit
-            // announcement finishes if there is no service left.
+            // This effectively schedules onAllServicesRemoved(), as it is to be called when the
+            // exit announcement finishes if there is no service left.
             // A non-zero exit announcement delay follows legacy mdnsresponder behavior, and is
             // also useful to ensure that when a host receives the exit announcement, the service
             // has been unregistered on all interfaces; so an announcement sent from interface A
@@ -294,9 +306,22 @@
         } else {
             // No exit announcement necessary: remove the service immediately.
             mRecordRepository.removeService(id);
-            if (mRecordRepository.getServicesCount() == 0) {
-                destroyNow();
-            }
+            mCbHandler.post(() -> {
+                if (mRecordRepository.getServicesCount() == 0) {
+                    mCb.onAllServicesRemoved(mSocket);
+                }
+            });
+        }
+        // Re-probe/re-announce the services which have the same custom hostname. These services
+        // were probed/announced using host addresses which were just removed so they should be
+        // re-probed/re-announced without those addresses.
+        if (hostname != null) {
+            final List<MdnsProber.ProbingInfo> probingInfos =
+                    mRecordRepository.restartProbingForHostname(hostname);
+            reprobeServices(probingInfos);
+            final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
+                    mRecordRepository.restartAnnouncingForHostname(hostname);
+            reannounceServices(announcementInfos);
         }
     }
 
@@ -330,7 +355,8 @@
     /**
      * Destroy the advertiser immediately, not sending any exit announcement.
      *
-     * <p>Useful when the underlying network went away. This will trigger an onDestroyed callback.
+     * <p>This is typically called when all services on the interface are removed or when the
+     * underlying network went away.
      */
     public void destroyNow() {
         for (int serviceId : mRecordRepository.clearServices()) {
@@ -339,7 +365,6 @@
         }
         mReplySender.cancelAll();
         mSocket.removePacketHandler(this);
-        mCbHandler.post(() -> mCb.onDestroyed(mSocket));
     }
 
     /**
@@ -442,4 +467,19 @@
             return new byte[0];
         }
     }
+
+    private void reprobeServices(List<MdnsProber.ProbingInfo> probingInfos) {
+        for (MdnsProber.ProbingInfo probingInfo : probingInfos) {
+            mProber.stop(probingInfo.getServiceId());
+            mProber.startProbing(probingInfo);
+        }
+    }
+
+    private void reannounceServices(List<MdnsAnnouncer.AnnouncementInfo> announcementInfos) {
+        for (MdnsAnnouncer.AnnouncementInfo announcementInfo : announcementInfos) {
+            mAnnouncer.stop(announcementInfo.getServiceId());
+            mAnnouncer.startSending(
+                    announcementInfo.getServiceId(), announcementInfo, 0 /* initialDelayMs */);
+        }
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsKeyRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsKeyRecord.java
new file mode 100644
index 0000000..ba8a56e
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsKeyRecord.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import static com.android.net.module.util.HexDump.toHexString;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/** An mDNS "KEY" record, which contains a public key for a name. See RFC 2535. */
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+public class MdnsKeyRecord extends MdnsRecord {
+    @Nullable private byte[] rData;
+
+    public MdnsKeyRecord(@NonNull String[] name, @NonNull MdnsPacketReader reader)
+            throws IOException {
+        this(name, reader, false);
+    }
+
+    public MdnsKeyRecord(@NonNull String[] name, @NonNull MdnsPacketReader reader,
+            boolean isQuestion) throws IOException {
+        super(name, TYPE_KEY, reader, isQuestion);
+    }
+
+    public MdnsKeyRecord(@NonNull String[] name, boolean isUnicast) {
+        super(name, TYPE_KEY,
+                MdnsConstants.QCLASS_INTERNET | (isUnicast ? MdnsConstants.QCLASS_UNICAST : 0),
+                0L /* receiptTimeMillis */, false /* cacheFlush */, 0L /* ttlMillis */);
+    }
+
+    public MdnsKeyRecord(@NonNull String[] name, long receiptTimeMillis, boolean cacheFlush,
+            long ttlMillis, @Nullable byte[] rData) {
+        super(name, TYPE_KEY, MdnsConstants.QCLASS_INTERNET, receiptTimeMillis, cacheFlush,
+                ttlMillis);
+        if (rData != null) {
+            this.rData = Arrays.copyOf(rData, rData.length);
+        }
+    }
+    /** Returns the KEY RDATA in bytes **/
+    public byte[] getRData() {
+        if (rData == null) {
+            return null;
+        }
+        return Arrays.copyOf(rData, rData.length);
+    }
+
+    @Override
+    protected void readData(MdnsPacketReader reader) throws IOException {
+        rData = new byte[reader.getRemaining()];
+        reader.readBytes(rData);
+    }
+
+    @Override
+    protected void writeData(MdnsPacketWriter writer) throws IOException {
+        if (rData != null) {
+            writer.writeBytes(rData);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "KEY: " + toHexString(rData);
+    }
+
+    @Override
+    public int hashCode() {
+        return (super.hashCode() * 31) + Arrays.hashCode(rData);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof MdnsKeyRecord)) {
+            return false;
+        }
+
+        return super.equals(other) && Arrays.equals(rData, ((MdnsKeyRecord) other).rData);
+    }
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index 869ac9b..c575d40 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -27,8 +27,11 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.util.ArrayMap;
+import android.util.Log;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -213,24 +216,36 @@
         return true;
     }
 
-    private void sendMdnsPacket(@NonNull DatagramPacket packet, @NonNull SocketKey targetSocketKey,
-            boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+    private void sendMdnsPackets(@NonNull List<DatagramPacket> packets,
+            @NonNull SocketKey targetSocketKey, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
         final MdnsInterfaceSocket socket = getTargetSocket(targetSocketKey);
         if (socket == null) {
             mSharedLog.e("No socket matches targetSocketKey=" + targetSocketKey);
             return;
         }
+        if (packets.isEmpty()) {
+            Log.wtf(TAG, "No mDns packets to send");
+            return;
+        }
+        // Check all packets with the same address
+        if (!MdnsUtils.checkAllPacketsWithSameAddress(packets)) {
+            Log.wtf(TAG, "Some mDNS packets have a different target address. addresses="
+                    + CollectionUtils.map(packets, DatagramPacket::getSocketAddress));
+            return;
+        }
 
-        final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
-                instanceof Inet6Address;
-        final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
-                instanceof Inet4Address;
+        final boolean isIpv6 = ((InetSocketAddress) packets.get(0).getSocketAddress())
+                .getAddress() instanceof Inet6Address;
+        final boolean isIpv4 = ((InetSocketAddress) packets.get(0).getSocketAddress())
+                .getAddress() instanceof Inet4Address;
         final boolean shouldQueryIpv6 = !onlyUseIpv6OnIpv6OnlyNetworks || !socket.hasJoinedIpv4();
         // Check ip capability and network before sending packet
         if ((isIpv6 && socket.hasJoinedIpv6() && shouldQueryIpv6)
                 || (isIpv4 && socket.hasJoinedIpv4())) {
             try {
-                socket.send(packet);
+                for (DatagramPacket packet : packets) {
+                    socket.send(packet);
+                }
             } catch (IOException e) {
                 mSharedLog.e("Failed to send a mDNS packet.", e);
             }
@@ -259,34 +274,34 @@
     }
 
     /**
-     * Send a mDNS request packet via given socket key that asks for multicast response.
+     * Send mDNS request packets via given socket key that asks for multicast response.
      */
-    public void sendPacketRequestingMulticastResponse(@NonNull DatagramPacket packet,
+    public void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
             @NonNull SocketKey socketKey, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
-        mHandler.post(() -> sendMdnsPacket(packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
+        mHandler.post(() -> sendMdnsPackets(packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
     }
 
     @Override
     public void sendPacketRequestingMulticastResponse(
-            @NonNull DatagramPacket packet, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+            @NonNull List<DatagramPacket> packets, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
         throw new UnsupportedOperationException("This socket client need to specify the socket to"
                 + "send packet");
     }
 
     /**
-     * Send a mDNS request packet via given socket key that asks for unicast response.
+     * Send mDNS request packets via given socket key that asks for unicast response.
      *
      * <p>The socket client may use a null network to identify some or all interfaces, in which case
      * passing null sends the packet to these.
      */
-    public void sendPacketRequestingUnicastResponse(@NonNull DatagramPacket packet,
+    public void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
             @NonNull SocketKey socketKey, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
-        mHandler.post(() -> sendMdnsPacket(packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
+        mHandler.post(() -> sendMdnsPackets(packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
     }
 
     @Override
     public void sendPacketRequestingUnicastResponse(
-            @NonNull DatagramPacket packet, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+            @NonNull List<DatagramPacket> packets, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
         throw new UnsupportedOperationException("This socket client need to specify the socket to"
                 + "send packet");
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
index 1239180..a5b8803 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
@@ -20,7 +20,7 @@
 import android.text.TextUtils;
 
 import com.android.net.module.util.CollectionUtils;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -153,7 +153,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(super.hashCode(),
-                Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(mNextDomain)),
+                Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(mNextDomain)),
                 Arrays.hashCode(mTypes));
     }
 
@@ -167,7 +167,7 @@
         }
 
         return super.equals(other)
-                && MdnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
+                && DnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
                 ((MdnsNsecRecord) other).mNextDomain)
                 && Arrays.equals(mTypes, ((MdnsNsecRecord) other).mTypes);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
index 1fabd49..aef8211 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
@@ -42,7 +42,7 @@
     @NonNull
     public final List<MdnsRecord> additionalRecords;
 
-    MdnsPacket(int flags,
+    public MdnsPacket(int flags,
             @NonNull List<MdnsRecord> questions,
             @NonNull List<MdnsRecord> answers,
             @NonNull List<MdnsRecord> authorityRecords,
@@ -196,6 +196,15 @@
                 }
             }
 
+            case MdnsRecord.TYPE_KEY: {
+                try {
+                    return new MdnsKeyRecord(name, reader, isQuestion);
+                } catch (IOException e) {
+                    throw new ParseException(MdnsResponseErrorCode.ERROR_READING_KEY_RDATA,
+                            "Failed to read KEY record from mDNS response.", e);
+                }
+            }
+
             case MdnsRecord.TYPE_NSEC: {
                 try {
                     return new MdnsNsecRecord(name, reader, isQuestion);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
index 6879a64..cf788be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -16,8 +16,8 @@
 
 package com.android.server.connectivity.mdns;
 
+import com.android.net.module.util.DnsUtils;
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -180,7 +180,7 @@
             int existingOffset = entry.getKey();
             String[] existingLabels = entry.getValue();
 
-            if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
+            if (DnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
                 writePointer(existingOffset);
                 return;
             } else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index e5c90a4..39bf653 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -20,7 +20,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -68,7 +68,7 @@
     }
 
     public boolean hasSubtype() {
-        return (name != null) && (name.length > 2) && MdnsUtils.equalsIgnoreDnsCase(name[1],
+        return (name != null) && (name.length > 2) && DnsUtils.equalsIgnoreDnsCase(name[1],
                 MdnsConstants.SUBTYPE_LABEL);
     }
 
@@ -83,7 +83,7 @@
 
     @Override
     public int hashCode() {
-        return (super.hashCode() * 31) + Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(pointer));
+        return (super.hashCode() * 31) + Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(pointer));
     }
 
     @Override
@@ -95,7 +95,7 @@
             return false;
         }
 
-        return super.equals(other) && MdnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
+        return super.equals(other) && DnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
                 ((MdnsPointerRecord) other).pointer);
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index e88947a..4a44fff 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -23,8 +23,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -114,7 +114,7 @@
         private static boolean containsName(@NonNull List<MdnsRecord> records,
                 @NonNull String[] name) {
             return CollectionUtils.any(records,
-                    r -> MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
+                    r -> DnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
         }
     }
 
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index 3fcf0d4..5c02767 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -63,17 +63,22 @@
      * rescheduling is not necessary.
      */
     @Nullable
-    public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(long now,
-            long minRemainingTtl, long lastSentTime, long sessionId) {
+    public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(
+            long now,
+            long minRemainingTtl,
+            long lastSentTime,
+            long sessionId,
+            int numOfQueriesBeforeBackoff) {
         if (mLastScheduledQueryTaskArgs == null) {
             return null;
         }
-        if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff()) {
+        if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
             return null;
         }
 
         final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
-                mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime);
+                mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime,
+                numOfQueriesBeforeBackoff);
 
         if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
             return null;
@@ -95,14 +100,16 @@
             long minRemainingTtl,
             long now,
             long lastSentTime,
-            long sessionId) {
-        final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun();
+            long sessionId,
+            int queryMode,
+            int numOfQueriesBeforeBackoff) {
+        final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
         final long timeToRun;
         if (mLastScheduledQueryTaskArgs == null) {
             timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
         } else {
             timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
-                    nextRunConfig, now, minRemainingTtl, lastSentTime);
+                    nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff);
         }
         mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
                 minRemainingTtl + now,
@@ -122,9 +129,10 @@
     }
 
     private static long calculateTimeToRun(@NonNull ScheduledQueryTaskArgs taskArgs,
-            QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime) {
+            QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
+            int numOfQueriesBeforeBackoff) {
         final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
-        if (!queryTaskConfig.shouldUseQueryBackoff()) {
+        if (!queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
             return lastSentTime + baseDelayInMs;
         }
         if (minRemainingTtl <= 0) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
index 4b43989..d464ca7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -23,8 +23,9 @@
 import android.os.SystemClock;
 import android.text.TextUtils;
 
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -40,6 +41,7 @@
     public static final int TYPE_PTR = 0x000C;
     public static final int TYPE_SRV = 0x0021;
     public static final int TYPE_TXT = 0x0010;
+    public static final int TYPE_KEY = 0x0019;
     public static final int TYPE_NSEC = 0x002f;
     public static final int TYPE_ANY = 0x00ff;
 
@@ -137,7 +139,7 @@
         }
 
         for (int i = 0; i < list1.length; ++i) {
-            if (!MdnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
+            if (!DnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
                 return false;
             }
         }
@@ -231,7 +233,7 @@
      * @param writer The writer to use.
      * @param now    The current system time. This is used when writing the updated TTL.
      */
-    @VisibleForTesting
+    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
     public final void write(MdnsPacketWriter writer, long now) throws IOException {
         writeHeaderFields(writer);
 
@@ -282,13 +284,13 @@
 
         MdnsRecord otherRecord = (MdnsRecord) other;
 
-        return MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
+        return DnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
                 == otherRecord.type);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(name)), type);
+        return Objects.hash(Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(name)), type);
     }
 
     /**
@@ -309,7 +311,7 @@
 
         public Key(int recordType, String[] recordName) {
             this.recordType = recordType;
-            this.recordName = MdnsUtils.toDnsLabelsLowerCase(recordName);
+            this.recordName = DnsUtils.toDnsLabelsUpperCase(recordName);
         }
 
         @Override
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index ac64c3a..c3cb776 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -34,9 +34,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.HexDump;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
@@ -87,6 +89,12 @@
     private static final String[] DNS_SD_SERVICE_TYPE =
             new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
 
+    private enum RecordConflictType {
+        NO_CONFLICT,
+        CONFLICT,
+        IDENTICAL
+    }
+
     @NonNull
     private final Random mDelayGenerator = new Random();
     // Map of service unique ID -> records for service
@@ -97,6 +105,8 @@
     @NonNull
     private final Looper mLooper;
     @NonNull
+    private final Dependencies mDeps;
+    @NonNull
     private final String[] mDeviceHostname;
     @NonNull
     private final MdnsFeatureFlags mMdnsFeatureFlags;
@@ -111,6 +121,7 @@
             @NonNull String[] deviceHostname, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
         mDeviceHostname = deviceHostname;
         mLooper = looper;
+        mDeps = deps;
         mMdnsFeatureFlags = mdnsFeatureFlags;
     }
 
@@ -127,6 +138,10 @@
         public Enumeration<InetAddress> getInterfaceInetAddresses(@NonNull NetworkInterface iface) {
             return iface.getInetAddresses();
         }
+
+        public long elapsedRealTime() {
+            return SystemClock.elapsedRealtime();
+        }
     }
 
     private static class RecordInfo<T extends MdnsRecord> {
@@ -140,17 +155,25 @@
         public final boolean isSharedName;
 
         /**
-         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast, 0 if never
+         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast on IPv4, 0
+         * if never
          */
-        public long lastAdvertisedTimeMs;
+        public long lastAdvertisedOnIpv4TimeMs;
 
         /**
-         * Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast,
-         * 0 if never
+         * Last time (as per SystemClock.elapsedRealtime) when advertised via multicast on IPv6, 0
+         * if never
          */
-        // FIXME: the `lastSentTimeMs` and `lastAdvertisedTimeMs` should be maintained separately
-        // for IPv4 and IPv6, because neither IPv4 nor and IPv6 clients can receive replies in
-        // different address space.
+        public long lastAdvertisedOnIpv6TimeMs;
+
+        /**
+         * Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast, 0 if
+         * never.
+         *
+         * <p>Different from lastAdvertisedOnIpv(4|6)TimeMs, lastSentTimeMs is mainly used for
+         * tracking is a record is ever sent out, no matter unicast/multicast or IPv4/IPv6. It's
+         * unnecessary to maintain two versions (IPv4/IPv6) for it.
+         */
         public long lastSentTimeMs;
 
         RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
@@ -169,6 +192,10 @@
         public final RecordInfo<MdnsServiceRecord> srvRecord;
         @Nullable
         public final RecordInfo<MdnsTextRecord> txtRecord;
+        @Nullable
+        public final RecordInfo<MdnsKeyRecord> serviceKeyRecord;
+        @Nullable
+        public final RecordInfo<MdnsKeyRecord> hostKeyRecord;
         @NonNull
         public final List<RecordInfo<MdnsInetAddressRecord>> addressRecords;
         @NonNull
@@ -200,11 +227,13 @@
         /**
          * Create a ServiceRegistration with only update the subType.
          */
-        ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+        ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes,
+                @NonNull MdnsFeatureFlags featureFlags) {
             NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
             newServiceInfo.setSubtypes(newSubtypes);
             return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
-                    repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
+                    repliedServiceCount, sentPacketCount, exiting, isProbing, ttl,
+                    featureFlags);
         }
 
         /**
@@ -212,7 +241,7 @@
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
                 int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
-                @Nullable Duration ttl) {
+                @Nullable Duration ttl, @NonNull MdnsFeatureFlags featureFlags) {
             this.serviceInfo = serviceInfo;
 
             final long nonNameRecordsTtlMillis;
@@ -230,7 +259,6 @@
                 nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
             }
 
-            final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
             final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname());
             final String[] hostname =
                     hasCustomHost
@@ -238,9 +266,11 @@
                             : deviceHostname;
             final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
 
-            if (hasService) {
-                final String[] serviceType = splitServiceType(serviceInfo);
-                final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
+            final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
+            final String[] serviceType = hasService ? splitServiceType(serviceInfo) : null;
+            final String[] serviceName =
+                    hasService ? splitFullyQualifiedName(serviceInfo, serviceType) : null;
+            if (hasService && hasSrvRecord(serviceInfo)) {
                 // Service PTR records
                 ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
                 ptrRecords.add(new RecordInfo<>(
@@ -282,7 +312,8 @@
                                 // Service name is verified unique after probing
                                 true /* cacheFlush */,
                                 nonNameRecordsTtlMillis,
-                                attrsToTextEntries(serviceInfo.getAttributes())),
+                                attrsToTextEntries(
+                                        serviceInfo.getAttributes(), featureFlags)),
                         false /* sharedName */);
 
                 allRecords.addAll(ptrRecords);
@@ -321,6 +352,36 @@
                 addressRecords = Collections.emptyList();
             }
 
+            final boolean hasKey = hasKeyRecord(serviceInfo);
+            if (hasKey && hasService) {
+                this.serviceKeyRecord = new RecordInfo<>(
+                        serviceInfo,
+                        new MdnsKeyRecord(
+                                serviceName,
+                                0L /*receiptTimeMillis */,
+                                true /* cacheFlush */,
+                                nameRecordsTtlMillis,
+                                serviceInfo.getPublicKey()),
+                        false /* sharedName */);
+                allRecords.add(this.serviceKeyRecord);
+            } else {
+                this.serviceKeyRecord = null;
+            }
+            if (hasKey && hasCustomHost) {
+                this.hostKeyRecord = new RecordInfo<>(
+                        serviceInfo,
+                        new MdnsKeyRecord(
+                                hostname,
+                                0L /*receiptTimeMillis */,
+                                true /* cacheFlush */,
+                                nameRecordsTtlMillis,
+                                serviceInfo.getPublicKey()),
+                        false /* sharedName */);
+                allRecords.add(this.hostKeyRecord);
+            } else {
+                this.hostKeyRecord = null;
+            }
+
             this.allRecords = Collections.unmodifiableList(allRecords);
             this.repliedServiceCount = repliedServiceCount;
             this.sentPacketCount = sentPacketCount;
@@ -335,9 +396,10 @@
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
+                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl,
+                @NonNull MdnsFeatureFlags featureFlags) {
             this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
-                    false /* exiting */, true /* isProbing */, ttl);
+                    false /* exiting */, true /* isProbing */, ttl, featureFlags);
         }
 
         void setProbing(boolean probing) {
@@ -388,7 +450,7 @@
                     "Service ID must already exist for an update request: " + serviceId);
         }
         final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
-                subtypes);
+                subtypes, mMdnsFeatureFlags);
         mServices.put(serviceId, updatedRegistration);
     }
 
@@ -419,7 +481,8 @@
 
         final ServiceRegistration registration = new ServiceRegistration(
                 mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
-                NO_PACKET /* sentPacketCount */, ttl);
+                NO_PACKET /* sentPacketCount */, ttl,
+                mMdnsFeatureFlags);
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -437,8 +500,8 @@
         }
         for (int i = 0; i < mServices.size(); i++) {
             final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
-            if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
-                    && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
+            if (DnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+                    && DnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
                 return mServices.keyAt(i);
             }
         }
@@ -471,11 +534,36 @@
                             ? inetAddressRecord.getInet6Address()
                             : inetAddressRecord.getInet4Address()));
         }
+
+        List<MdnsKeyRecord> keyRecords = new ArrayList<>();
+        if (registration.serviceKeyRecord != null) {
+            keyRecords.add(registration.serviceKeyRecord.record);
+        }
+        if (registration.hostKeyRecord != null) {
+            keyRecords.add(registration.hostKeyRecord.record);
+        }
+        for (MdnsKeyRecord keyRecord : keyRecords) {
+            probingRecords.add(new MdnsKeyRecord(
+                            keyRecord.getName(),
+                            0L /* receiptTimeMillis */,
+                            false /* cacheFlush */,
+                            keyRecord.getTtl(),
+                            keyRecord.getRData()));
+        }
         return new MdnsProber.ProbingInfo(serviceId, probingRecords);
     }
 
-    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(Map<String, byte[]> attrs) {
-        final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(attrs.size());
+    private static List<MdnsServiceInfo.TextEntry> attrsToTextEntries(
+            @NonNull Map<String, byte[]> attrs, @NonNull MdnsFeatureFlags featureFlags) {
+        final List<MdnsServiceInfo.TextEntry> out = new ArrayList<>(
+                attrs.size() == 0 ? 1 : attrs.size());
+        if (featureFlags.avoidAdvertisingEmptyTxtRecords() && attrs.size() == 0) {
+            // As per RFC6763 6.1, empty TXT records are not allowed, but records containing a
+            // single empty String must be treated as equivalent.
+            out.add(new MdnsServiceInfo.TextEntry("", MdnsServiceInfo.TextEntry.VALUE_NONE));
+            return out;
+        }
+
         for (Map.Entry<String, byte[]> attr : attrs.entrySet()) {
             out.add(new MdnsServiceInfo.TextEntry(attr.getKey(), attr.getValue()));
         }
@@ -577,7 +665,8 @@
      */
     @Nullable
     public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
-        final long now = SystemClock.elapsedRealtime();
+        final long now = mDeps.elapsedRealTime();
+        final boolean isQuestionOnIpv4 = src.getAddress() instanceof Inet4Address;
 
         // TODO: b/322142420 - Set<RecordInfo<?>> may contain duplicate records wrapped in different
         // RecordInfo<?>s when custom host is enabled.
@@ -595,7 +684,7 @@
                     null /* serviceSrvRecord */, null /* serviceTxtRecord */,
                     null /* hostname */,
                     replyUnicastEnabled, now, answerInfo, additionalAnswerInfo,
-                    Collections.emptyList())) {
+                    Collections.emptyList(), isQuestionOnIpv4)) {
                 replyUnicast &= question.isUnicastReplyRequested();
             }
 
@@ -607,7 +696,7 @@
                         registration.srvRecord, registration.txtRecord,
                         registration.serviceInfo.getHostname(),
                         replyUnicastEnabled, now,
-                        answerInfo, additionalAnswerInfo, packet.answers)) {
+                        answerInfo, additionalAnswerInfo, packet.answers, isQuestionOnIpv4)) {
                     replyUnicast &= question.isUnicastReplyRequested();
                     registration.repliedServiceCount++;
                     registration.sentPacketCount++;
@@ -685,7 +774,7 @@
             // multicast responses. Unicast replies are faster as they do not need to wait for the
             // beacon interval on Wi-Fi.
             dest = src;
-        } else if (src.getAddress() instanceof Inet4Address) {
+        } else if (isQuestionOnIpv4) {
             dest = IPV4_SOCKET_ADDR;
         } else {
             dest = IPV6_SOCKET_ADDR;
@@ -697,7 +786,11 @@
             // TODO: consider actual packet send delay after response aggregation
             info.lastSentTimeMs = now + delayMs;
             if (!replyUnicast) {
-                info.lastAdvertisedTimeMs = info.lastSentTimeMs;
+                if (isQuestionOnIpv4) {
+                    info.lastAdvertisedOnIpv4TimeMs = info.lastSentTimeMs;
+                } else {
+                    info.lastAdvertisedOnIpv6TimeMs = info.lastSentTimeMs;
+                }
             }
             // Different RecordInfos may the contain the same record
             if (!answerRecords.contains(info.record)) {
@@ -729,7 +822,8 @@
             @Nullable String hostname,
             boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo,
             @NonNull Set<RecordInfo<?>> additionalAnswerInfo,
-            @NonNull List<MdnsRecord> knownAnswerRecords) {
+            @NonNull List<MdnsRecord> knownAnswerRecords,
+            boolean isQuestionOnIpv4) {
         boolean hasDnsSdPtrRecordAnswer = false;
         boolean hasDnsSdSrvRecordAnswer = false;
         boolean hasFullyOwnedNameMatch = false;
@@ -742,7 +836,7 @@
              must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
              "CNAME" (5), and the record rrclass must match the question qclass unless the
              qclass is "ANY" (255) */
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
                 continue;
             }
             hasFullyOwnedNameMatch |= !info.isSharedName;
@@ -778,10 +872,20 @@
 
             // TODO: responses to probe queries should bypass this check and only ensure the
             // reply is sent 250ms after the last sent time (RFC 6762 p.15)
-            if (!(replyUnicastEnabled && question.isUnicastReplyRequested())
-                    && info.lastAdvertisedTimeMs > 0L
-                    && now - info.lastAdvertisedTimeMs < MIN_MULTICAST_REPLY_INTERVAL_MS) {
-                continue;
+            if (!(replyUnicastEnabled && question.isUnicastReplyRequested())) {
+                if (isQuestionOnIpv4) { // IPv4
+                    if (info.lastAdvertisedOnIpv4TimeMs > 0L
+                            && now - info.lastAdvertisedOnIpv4TimeMs
+                                    < MIN_MULTICAST_REPLY_INTERVAL_MS) {
+                        continue;
+                    }
+                } else { // IPv6
+                    if (info.lastAdvertisedOnIpv6TimeMs > 0L
+                            && now - info.lastAdvertisedOnIpv6TimeMs
+                                    < MIN_MULTICAST_REPLY_INTERVAL_MS) {
+                        continue;
+                    }
+                }
             }
 
             answerInfo.add(info);
@@ -925,22 +1029,79 @@
         }
     }
 
+    @Nullable
+    public String getHostnameForServiceId(int id) {
+        ServiceRegistration registration = mServices.get(id);
+        if (registration == null) {
+            return null;
+        }
+        return registration.serviceInfo.getHostname();
+    }
+
+    /**
+     * Restart probing the services which are being probed and using the given custom hostname.
+     *
+     * @return The list of {@link MdnsProber.ProbingInfo} to be used by advertiser.
+     */
+    public List<MdnsProber.ProbingInfo> restartProbingForHostname(@NonNull String hostname) {
+        final ArrayList<MdnsProber.ProbingInfo> probingInfos = new ArrayList<>();
+        forEachActiveServiceRegistrationWithHostname(
+                hostname,
+                (id, registration) -> {
+                    if (!registration.isProbing) {
+                        return;
+                    }
+                    probingInfos.add(makeProbingInfo(id, registration));
+                });
+        return probingInfos;
+    }
+
+    /**
+     * Restart announcing the services which are using the given custom hostname.
+     *
+     * @return The list of {@link MdnsAnnouncer.AnnouncementInfo} to be used by advertiser.
+     */
+    public List<MdnsAnnouncer.AnnouncementInfo> restartAnnouncingForHostname(
+            @NonNull String hostname) {
+        final ArrayList<MdnsAnnouncer.AnnouncementInfo> announcementInfos = new ArrayList<>();
+        forEachActiveServiceRegistrationWithHostname(
+                hostname,
+                (id, registration) -> {
+                    if (registration.isProbing) {
+                        return;
+                    }
+                    announcementInfos.add(makeAnnouncementInfo(id, registration));
+                });
+        return announcementInfos;
+    }
+
     /**
      * Called to indicate that probing succeeded for a service.
+     *
      * @param probeSuccessInfo The successful probing info.
      * @return The {@link MdnsAnnouncer.AnnouncementInfo} to send, now that probing has succeeded.
      */
     public MdnsAnnouncer.AnnouncementInfo onProbingSucceeded(
-            MdnsProber.ProbingInfo probeSuccessInfo)
-            throws IOException {
-
-        int serviceId = probeSuccessInfo.getServiceId();
+            MdnsProber.ProbingInfo probeSuccessInfo) throws IOException {
+        final int serviceId = probeSuccessInfo.getServiceId();
         final ServiceRegistration registration = mServices.get(serviceId);
         if (registration == null) {
             throw new IOException("Service is not registered: " + serviceId);
         }
         registration.setProbing(false);
 
+        return makeAnnouncementInfo(serviceId, registration);
+    }
+
+    /**
+     * Make the announcement info of the given service ID.
+     *
+     * @param serviceId The service ID.
+     * @param registration The service registration.
+     * @return The {@link MdnsAnnouncer.AnnouncementInfo} of the given service ID.
+     */
+    private MdnsAnnouncer.AnnouncementInfo makeAnnouncementInfo(
+            int serviceId, ServiceRegistration registration) {
         final Set<MdnsRecord> answersSet = new LinkedHashSet<>();
         final ArrayList<MdnsRecord> additionalAnswers = new ArrayList<>();
 
@@ -972,8 +1133,8 @@
         addNsecRecordsForUniqueNames(additionalAnswers,
                 mGeneralRecords.iterator(), registration.allRecords.iterator());
 
-        return new MdnsAnnouncer.AnnouncementInfo(
-                probeSuccessInfo.getServiceId(), new ArrayList<>(answersSet), additionalAnswers);
+        return new MdnsAnnouncer.AnnouncementInfo(serviceId,
+                new ArrayList<>(answersSet), additionalAnswers);
     }
 
     /**
@@ -1013,18 +1174,15 @@
                 Collections.emptyList() /* additionalRecords */);
     }
 
-    /** Check if the record is in any service registration */
-    private boolean hasInetAddressRecord(@NonNull MdnsInetAddressRecord record) {
-        for (int i = 0; i < mServices.size(); i++) {
-            final ServiceRegistration registration = mServices.valueAt(i);
-            if (registration.exiting) continue;
-
-            for (RecordInfo<MdnsInetAddressRecord> localRecord : registration.addressRecords) {
-                if (Objects.equals(localRecord.record, record)) {
-                    return true;
-                }
+    /** Check if the record is in a registration */
+    private static boolean hasInetAddressRecord(
+            @NonNull ServiceRegistration registration, @NonNull MdnsInetAddressRecord record) {
+        for (RecordInfo<MdnsInetAddressRecord> localRecord : registration.addressRecords) {
+            if (Objects.equals(localRecord.record, record)) {
+                return true;
             }
         }
+
         return false;
     }
 
@@ -1036,99 +1194,125 @@
      * {@link MdnsInterfaceAdvertiser#CONFLICT_HOST}.
      */
     public Map<Integer, Integer> getConflictingServices(MdnsPacket packet) {
-        // Avoid allocating a new set for each incoming packet: use an empty set by default.
-        Map<Integer, Integer> conflicting = Collections.emptyMap();
+        Map<Integer, Integer> conflicting = new ArrayMap<>();
         for (MdnsRecord record : packet.answers) {
+            SparseIntArray conflictingWithRecord = new SparseIntArray();
             for (int i = 0; i < mServices.size(); i++) {
                 final ServiceRegistration registration = mServices.valueAt(i);
                 if (registration.exiting) continue;
 
-                int conflictType = 0;
+                final RecordConflictType conflictForService =
+                        conflictForService(record, registration);
+                final RecordConflictType conflictForHost = conflictForHost(record, registration);
 
-                if (conflictForService(record, registration)) {
-                    conflictType |= CONFLICT_SERVICE;
+                // Identical record is found in the repository so there won't be a conflict.
+                if (conflictForService == RecordConflictType.IDENTICAL
+                        || conflictForHost == RecordConflictType.IDENTICAL) {
+                    conflictingWithRecord.clear();
+                    break;
                 }
 
-                if (conflictForHost(record, registration)) {
+                int conflictType = 0;
+                if (conflictForService == RecordConflictType.CONFLICT) {
+                    conflictType |= CONFLICT_SERVICE;
+                }
+                if (conflictForHost == RecordConflictType.CONFLICT) {
                     conflictType |= CONFLICT_HOST;
                 }
 
                 if (conflictType != 0) {
-                    if (conflicting.isEmpty()) {
-                        // Conflict was found: use a mutable set
-                        conflicting = new ArrayMap<>();
-                    }
                     final int serviceId = mServices.keyAt(i);
-                    conflicting.put(serviceId, conflictType);
+                    conflictingWithRecord.put(serviceId, conflictType);
                 }
             }
+            for (int i = 0; i < conflictingWithRecord.size(); i++) {
+                final int serviceId = conflictingWithRecord.keyAt(i);
+                final int conflictType = conflictingWithRecord.valueAt(i);
+                final int oldConflictType = conflicting.getOrDefault(serviceId, 0);
+                conflicting.put(serviceId, oldConflictType | conflictType);
+            }
         }
 
         return conflicting;
     }
 
-
-    private static boolean conflictForService(
+    private static RecordConflictType conflictForService(
             @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
-        if (registration.srvRecord == null) {
-            return false;
+        String[] fullServiceName;
+        if (registration.srvRecord != null) {
+            fullServiceName = registration.srvRecord.record.getName();
+        } else if (registration.serviceKeyRecord != null) {
+            fullServiceName = registration.serviceKeyRecord.record.getName();
+        } else {
+            return RecordConflictType.NO_CONFLICT;
         }
 
-        final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
-        if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), srvRecord.record.getName())) {
-            return false;
+        if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), fullServiceName)) {
+            return RecordConflictType.NO_CONFLICT;
         }
 
         // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
         // data.
-        if (record instanceof MdnsServiceRecord) {
-            final MdnsServiceRecord local = srvRecord.record;
-            final MdnsServiceRecord other = (MdnsServiceRecord) record;
-            // Note "equals" does not consider TTL or receipt time, as intended here
-            if (Objects.equals(local, other)) {
-                return false;
-            }
+        if (record instanceof MdnsServiceRecord && equals(record, registration.srvRecord)) {
+            return RecordConflictType.IDENTICAL;
+        }
+        if (record instanceof MdnsTextRecord && equals(record, registration.txtRecord)) {
+            return RecordConflictType.IDENTICAL;
+        }
+        if (record instanceof MdnsKeyRecord && equals(record, registration.serviceKeyRecord)) {
+            return RecordConflictType.IDENTICAL;
         }
 
-        if (record instanceof MdnsTextRecord) {
-            final MdnsTextRecord local = registration.txtRecord.record;
-            final MdnsTextRecord other = (MdnsTextRecord) record;
-            if (Objects.equals(local, other)) {
-                return false;
-            }
-        }
-        return true;
+        return RecordConflictType.CONFLICT;
     }
 
-    private boolean conflictForHost(
+    private RecordConflictType conflictForHost(
             @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
         // Only custom hosts are checked. When using the default host, the hostname is derived from
         // a UUID and it's supposed to be unique.
         if (registration.serviceInfo.getHostname() == null) {
-            return false;
+            return RecordConflictType.NO_CONFLICT;
+        }
+
+        // It cannot be a hostname conflict because no record is registered with the hostname.
+        if (registration.addressRecords.isEmpty() && registration.hostKeyRecord == null) {
+            return RecordConflictType.NO_CONFLICT;
         }
 
         // The record's name cannot be registered by NsdManager so it's not a conflict.
         if (record.getName().length != 2 || !record.getName()[1].equals(LOCAL_TLD)) {
-            return false;
+            return RecordConflictType.NO_CONFLICT;
         }
 
         // Different names. There won't be a conflict.
-        if (!MdnsUtils.equalsIgnoreDnsCase(
+        if (!DnsUtils.equalsIgnoreDnsCase(
                 record.getName()[0], registration.serviceInfo.getHostname())) {
-            return false;
+            return RecordConflictType.NO_CONFLICT;
         }
 
-        // If this registration has any address record and there's no identical record in the
-        // repository, it's a conflict. There will be no conflict if no registration has addresses
-        // for that hostname.
-        if (record instanceof MdnsInetAddressRecord) {
-            if (!registration.addressRecords.isEmpty()) {
-                return !hasInetAddressRecord((MdnsInetAddressRecord) record);
-            }
+        // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
+        // data.
+        if (record instanceof MdnsInetAddressRecord
+                && hasInetAddressRecord(registration, (MdnsInetAddressRecord) record)) {
+            return RecordConflictType.IDENTICAL;
+        }
+        if (record instanceof MdnsKeyRecord && equals(record, registration.hostKeyRecord)) {
+            return RecordConflictType.IDENTICAL;
         }
 
-        return false;
+        // Per RFC 6762 8.1, when a record is being probed, any answer containing a record with that
+        // name, of any type, MUST be considered a conflicting response.
+        if (registration.isProbing) {
+            return RecordConflictType.CONFLICT;
+        }
+        if (record instanceof MdnsInetAddressRecord && !registration.addressRecords.isEmpty()) {
+            return RecordConflictType.CONFLICT;
+        }
+        if (record instanceof MdnsKeyRecord && registration.hostKeyRecord != null) {
+            return RecordConflictType.CONFLICT;
+        }
+
+        return RecordConflictType.NO_CONFLICT;
     }
 
     private List<RecordInfo<MdnsInetAddressRecord>> getInetAddressRecordsForHostname(
@@ -1182,7 +1366,7 @@
             int id = mServices.keyAt(i);
             ServiceRegistration service = mServices.valueAt(i);
             if (service.exiting) continue;
-            if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
+            if (DnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
                 consumer.accept(id, service);
             }
         }
@@ -1233,7 +1417,8 @@
         if (existing == null) return null;
 
         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
-                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
+                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl,
+                mMdnsFeatureFlags);
         mServices.put(serviceId, newService);
         return makeProbingInfo(serviceId, newService);
     }
@@ -1245,10 +1430,11 @@
         final ServiceRegistration registration = mServices.get(serviceId);
         if (registration == null) return;
 
-        final long now = SystemClock.elapsedRealtime();
+        final long now = mDeps.elapsedRealTime();
         for (RecordInfo<?> record : registration.allRecords) {
             record.lastSentTimeMs = now;
-            record.lastAdvertisedTimeMs = now;
+            record.lastAdvertisedOnIpv4TimeMs = now;
+            record.lastAdvertisedOnIpv6TimeMs = now;
         }
         registration.sentPacketCount += sentPacketCount;
     }
@@ -1313,4 +1499,21 @@
 
         return type;
     }
+
+    /** Returns whether there will be an SRV record when registering the {@code info}. */
+    private static boolean hasSrvRecord(@NonNull NsdServiceInfo info) {
+        return info.getPort() > 0;
+    }
+
+    /** Returns whether there will be KEY record(s) when registering the {@code info}. */
+    private static boolean hasKeyRecord(@NonNull NsdServiceInfo info) {
+        return info.getPublicKey() != null;
+    }
+
+    private static boolean equals(@NonNull MdnsRecord record, @Nullable RecordInfo<?> recordInfo) {
+        if (recordInfo == null) {
+            return false;
+        }
+        return Objects.equals(record, recordInfo.record);
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index 05ad1be..3636644 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -21,7 +21,7 @@
 import android.net.Network;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -111,7 +111,7 @@
      * pointer record is already present in the response with the same TTL.
      */
     public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
-        if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
+        if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
             throw new IllegalArgumentException(
                     "Pointer records for different service names cannot be added");
         }
@@ -305,13 +305,13 @@
         boolean dropAddressRecords = false;
 
         for (MdnsInetAddressRecord inetAddressRecord : getInet4AddressRecords()) {
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
                     this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
                 dropAddressRecords = true;
             }
         }
         for (MdnsInetAddressRecord inetAddressRecord : getInet6AddressRecords()) {
-            if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+            if (!DnsUtils.equalsDnsLabelIgnoreDnsCase(
                     this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
                 dropAddressRecords = true;
             }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index b812bb4..52e76ad 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -22,6 +22,7 @@
 import android.util.ArrayMap;
 import android.util.Pair;
 
+import com.android.net.module.util.DnsUtils;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.EOFException;
@@ -49,7 +50,7 @@
             List<MdnsResponse> responses, String[] pointer) {
         if (responses != null) {
             for (MdnsResponse response : responses) {
-                if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
+                if (DnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
                     return response;
                 }
             }
@@ -65,7 +66,7 @@
                 if (serviceRecord == null) {
                     continue;
                 }
-                if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
+                if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
                         hostName)) {
                     return response;
                 }
@@ -318,7 +319,7 @@
             if (serviceRecord == null) {
                 continue;
             }
-            if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
+            if (DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
                 if (result == null) {
                     result = new ArrayList<>(/* initialCapacity= */ responses.size());
                 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
index 73a7e3a..f509da2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
@@ -37,4 +37,5 @@
     public static final int ERROR_END_OF_FILE = 12;
     public static final int ERROR_READING_NSEC_RDATA = 13;
     public static final int ERROR_READING_ANY_RDATA = 14;
+    public static final int ERROR_READING_KEY_RDATA = 15;
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
index 086094b..73405ab 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -59,6 +59,7 @@
                             source.readInt(),
                             source.readInt() == 1,
                             source.readParcelable(null),
+                            source.readInt(),
                             source.readString(),
                             source.readInt() == 1,
                             source.readInt());
@@ -79,6 +80,8 @@
     private final boolean removeExpiredService;
     // The target network for searching. Null network means search on all possible interfaces.
     @Nullable private final Network mNetwork;
+    // If the target interface does not have a Network, set to the interface index, otherwise unset.
+    private final int mInterfaceIndex;
 
     /** Parcelable constructs for a {@link MdnsSearchOptions}. */
     MdnsSearchOptions(
@@ -86,6 +89,7 @@
             int queryMode,
             boolean removeExpiredService,
             @Nullable Network network,
+            int interfaceIndex,
             @Nullable String resolveInstanceName,
             boolean onlyUseIpv6OnIpv6OnlyNetworks,
             int numOfQueriesBeforeBackoff) {
@@ -98,6 +102,7 @@
         this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
         this.removeExpiredService = removeExpiredService;
         mNetwork = network;
+        mInterfaceIndex = interfaceIndex;
         this.resolveInstanceName = resolveInstanceName;
     }
 
@@ -148,15 +153,27 @@
     }
 
     /**
-     * Returns the network which the mdns query should target on.
+     * Returns the network which the mdns query should target.
      *
-     * @return the target network or null if search on all possible interfaces.
+     * @return the target network or null to search on all possible interfaces.
      */
     @Nullable
     public Network getNetwork() {
         return mNetwork;
     }
 
+
+    /**
+     * Returns the interface index which the mdns query should target.
+     *
+     * This is only set when the service is to be searched on an interface that does not have a
+     * Network, in which case {@link #getNetwork()} returns null.
+     * The interface index as per {@link java.net.NetworkInterface#getIndex}, or 0 if unset.
+     */
+    public int getInterfaceIndex() {
+        return mInterfaceIndex;
+    }
+
     /**
      * If non-null, queries should try to resolve all records of this specific service, rather than
      * discovering all services.
@@ -177,6 +194,7 @@
         out.writeInt(queryMode);
         out.writeInt(removeExpiredService ? 1 : 0);
         out.writeParcelable(mNetwork, 0);
+        out.writeInt(mInterfaceIndex);
         out.writeString(resolveInstanceName);
         out.writeInt(onlyUseIpv6OnIpv6OnlyNetworks ? 1 : 0);
         out.writeInt(numOfQueriesBeforeBackoff);
@@ -190,6 +208,7 @@
         private int numOfQueriesBeforeBackoff = 3;
         private boolean removeExpiredService;
         private Network mNetwork;
+        private int mInterfaceIndex;
         private String resolveInstanceName;
 
         private Builder() {
@@ -278,6 +297,16 @@
             return this;
         }
 
+        /**
+         * Set the interface index to use for the query, if not querying on a {@link Network}.
+         *
+         * @see MdnsSearchOptions#getInterfaceIndex()
+         */
+        public Builder setInterfaceIndex(int index) {
+            mInterfaceIndex = index;
+            return this;
+        }
+
         /** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
         public MdnsSearchOptions build() {
             return new MdnsSearchOptions(
@@ -285,6 +314,7 @@
                     queryMode,
                     removeExpiredService,
                     mNetwork,
+                    mInterfaceIndex,
                     resolveInstanceName,
                     onlyUseIpv6OnIpv6OnlyNetworks,
                     numOfQueriesBeforeBackoff);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index e9a41d1..a8a4ef1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,10 +16,10 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.toDnsUpperCase;
 import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
 
 import static java.lang.Math.min;
 
@@ -49,16 +49,16 @@
  */
 public class MdnsServiceCache {
     static class CacheKey {
-        @NonNull final String mLowercaseServiceType;
+        @NonNull final String mUpperCaseServiceType;
         @NonNull final SocketKey mSocketKey;
 
         CacheKey(@NonNull String serviceType, @NonNull SocketKey socketKey) {
-            mLowercaseServiceType = toDnsLowerCase(serviceType);
+            mUpperCaseServiceType = toDnsUpperCase(serviceType);
             mSocketKey = socketKey;
         }
 
         @Override public int hashCode() {
-            return Objects.hash(mLowercaseServiceType, mSocketKey);
+            return Objects.hash(mUpperCaseServiceType, mSocketKey);
         }
 
         @Override public boolean equals(Object other) {
@@ -68,7 +68,7 @@
             if (!(other instanceof CacheKey)) {
                 return false;
             }
-            return Objects.equals(mLowercaseServiceType, ((CacheKey) other).mLowercaseServiceType)
+            return Objects.equals(mUpperCaseServiceType, ((CacheKey) other).mUpperCaseServiceType)
                     && Objects.equals(mSocketKey, ((CacheKey) other).mSocketKey);
         }
     }
@@ -233,6 +233,21 @@
     }
 
     /**
+     * Remove services which matches the given type and socket.
+     *
+     * @param cacheKey the target CacheKey.
+     */
+    public void removeServices(@NonNull CacheKey cacheKey) {
+        ensureRunningOnHandlerThread(mHandler);
+        // Remove all services
+        if (mCachedServices.remove(cacheKey) == null) {
+            return;
+        }
+        // Update the next expiration check time if services are removed.
+        mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+    }
+
+    /**
      * Register a callback to listen to service expiration.
      *
      * <p> Registering the same callback instance twice is a no-op, since MdnsServiceTypeClient
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index f60a95e..a16fcf7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -16,8 +16,6 @@
 
 package com.android.server.connectivity.mdns;
 
-import static com.android.server.connectivity.mdns.MdnsSocket.INTERFACE_INDEX_UNSPECIFIED;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
@@ -33,7 +31,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
 
@@ -94,57 +91,6 @@
     @NonNull
     private final Instant expirationTime;
 
-    /** Constructs a {@link MdnsServiceInfo} object with default values. */
-    public MdnsServiceInfo(
-            String serviceInstanceName,
-            String[] serviceType,
-            @Nullable List<String> subtypes,
-            String[] hostName,
-            int port,
-            @Nullable String ipv4Address,
-            @Nullable String ipv6Address,
-            @Nullable List<String> textStrings) {
-        this(
-                serviceInstanceName,
-                serviceType,
-                subtypes,
-                hostName,
-                port,
-                List.of(ipv4Address),
-                List.of(ipv6Address),
-                textStrings,
-                /* textEntries= */ null,
-                /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
-                /* network= */ null,
-                /* expirationTime= */ Instant.MAX);
-    }
-
-    /** Constructs a {@link MdnsServiceInfo} object with default values. */
-    public MdnsServiceInfo(
-            String serviceInstanceName,
-            String[] serviceType,
-            List<String> subtypes,
-            String[] hostName,
-            int port,
-            @Nullable String ipv4Address,
-            @Nullable String ipv6Address,
-            @Nullable List<String> textStrings,
-            @Nullable List<TextEntry> textEntries) {
-        this(
-                serviceInstanceName,
-                serviceType,
-                subtypes,
-                hostName,
-                port,
-                List.of(ipv4Address),
-                List.of(ipv6Address),
-                textStrings,
-                textEntries,
-                /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
-                /* network= */ null,
-                /* expirationTime= */ Instant.MAX);
-    }
-
     /**
      * Constructs a {@link MdnsServiceInfo} object with default values.
      *
@@ -406,6 +352,13 @@
 
     /** Represents a DNS TXT key-value pair defined by RFC 6763. */
     public static final class TextEntry implements Parcelable {
+        /**
+         * The value to use for attributes with no value.
+         *
+         * <p>As per RFC6763 P.16, attributes may have no value, which is different from having an
+         * empty value (which would be an empty byte array).
+         */
+        public static final byte[] VALUE_NONE = null;
         public static final Parcelable.Creator<TextEntry> CREATOR =
                 new Parcelable.Creator<TextEntry>() {
                     @Override
@@ -440,7 +393,7 @@
             // 2. If there is no '=' in a DNS-SD TXT record string, then it is a
             // boolean attribute, simply identified as being present, with no value.
             if (delimitPos < 0) {
-                return new TextEntry(new String(textBytes, US_ASCII), (byte[]) null);
+                return new TextEntry(new String(textBytes, US_ASCII), VALUE_NONE);
             } else if (delimitPos == 0) {
                 return null;
             }
@@ -451,13 +404,13 @@
 
         /** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */
         public TextEntry(String key, String value) {
-            this(key, value == null ? null : value.getBytes(UTF_8));
+            this(key, value == null ? VALUE_NONE : value.getBytes(UTF_8));
         }
 
         /** Creates a new {@link TextEntry} with given key and value of a byte array. */
         public TextEntry(String key, byte[] value) {
             this.key = key;
-            this.value = value == null ? null : value.clone();
+            this.value = value == VALUE_NONE ? VALUE_NONE : value.clone();
         }
 
         private TextEntry(Parcel in) {
@@ -470,22 +423,26 @@
         }
 
         public byte[] getValue() {
-            return value == null ? null : value.clone();
+            return value == VALUE_NONE ? VALUE_NONE : value.clone();
         }
 
         /** Converts this {@link TextEntry} instance to '=' separated byte array. */
         public byte[] toBytes() {
             final byte[] keyBytes = key.getBytes(US_ASCII);
-            if (value == null) {
+            if (value == VALUE_NONE) {
                 return keyBytes;
             }
             return ByteUtils.concat(keyBytes, new byte[]{'='}, value);
         }
 
+        public boolean isEmpty() {
+            return TextUtils.isEmpty(key) && (value == VALUE_NONE || value.length == 0);
+        }
+
         /** Converts this {@link TextEntry} instance to '=' separated string. */
         @Override
         public String toString() {
-            if (value == null) {
+            if (value == VALUE_NONE) {
                 return key;
             }
             return key + "=" + new String(value, UTF_8);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index 0d6a9ec..fd716d2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -20,7 +20,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.server.connectivity.mdns.util.MdnsUtils;
+import com.android.net.module.util.DnsUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -151,7 +151,7 @@
     public int hashCode() {
         return (super.hashCode() * 31)
                 + Objects.hash(servicePriority, serviceWeight,
-                Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(serviceHost)),
+                Arrays.hashCode(DnsUtils.toDnsLabelsUpperCase(serviceHost)),
                 servicePort);
     }
 
@@ -168,7 +168,7 @@
         return super.equals(other)
                 && (servicePriority == otherRecord.servicePriority)
                 && (serviceWeight == otherRecord.serviceWeight)
-                && MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
+                && DnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
                 && (servicePort == otherRecord.servicePort);
     }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 8f41b94..8959c1b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -30,14 +30,19 @@
 import android.util.ArrayMap;
 import android.util.Pair;
 
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
+import java.io.IOException;
 import java.io.PrintWriter;
+import java.net.DatagramPacket;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.net.InetSocketAddress;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -55,7 +60,6 @@
 public class MdnsServiceTypeClient {
 
     private static final String TAG = MdnsServiceTypeClient.class.getSimpleName();
-    private static final int DEFAULT_MTU = 1500;
     @VisibleForTesting
     static final int EVENT_START_QUERYTASK = 1;
     static final int EVENT_QUERY_RESULT = 2;
@@ -84,6 +88,7 @@
                     notifyRemovedServiceToListeners(previousResponse, "Service record expired");
                 }
             };
+    @NonNull private final MdnsFeatureFlags featureFlags;
     private final ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners =
             new ArrayMap<>();
     private final boolean removeServiceAfterTtlExpires =
@@ -116,11 +121,11 @@
          * @return true if the service name was not discovered before.
          */
         boolean setServiceDiscovered(@NonNull String serviceName) {
-            return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+            return discoveredServiceNames.add(DnsUtils.toDnsUpperCase(serviceName));
         }
 
         void unsetServiceDiscovered(@NonNull String serviceName) {
-            discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+            discoveredServiceNames.remove(DnsUtils.toDnsUpperCase(serviceName));
         }
     }
 
@@ -142,7 +147,9 @@
                     // before sending the query, it needs to be called just before sending it.
                     final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
                     final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
-                            getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
+                            getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+                            getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
+                            socketKey);
                     executor.submit(queryTask);
                     break;
                 }
@@ -173,7 +180,9 @@
                                     minRemainingTtl,
                                     now,
                                     lastSentTime,
-                                    sentResult.taskArgs.sessionId
+                                    sentResult.taskArgs.sessionId,
+                                    searchOptions.getQueryMode(),
+                                    searchOptions.numOfQueriesBeforeBackoff()
                             );
                     dependencies.sendMessageDelayed(
                             handler,
@@ -191,7 +200,7 @@
     /**
      * Dependencies of MdnsServiceTypeClient, for injection in tests.
      */
-    @VisibleForTesting
+    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
     public static class Dependencies {
         /**
          * @see Handler#sendMessageDelayed(Message, long)
@@ -221,6 +230,25 @@
         public void sendMessage(@NonNull Handler handler, @NonNull Message message) {
             handler.sendMessage(message);
         }
+
+        /**
+         * Generate the DatagramPackets from given MdnsPacket and InetSocketAddress.
+         *
+         * <p> If the query with known answer feature is enabled and the MdnsPacket is too large for
+         *     a single DatagramPacket, it will be split into multiple DatagramPackets.
+         */
+        public List<DatagramPacket> getDatagramPacketsFromMdnsPacket(
+                @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet,
+                @NonNull InetSocketAddress address, boolean isQueryWithKnownAnswer)
+                throws IOException {
+            if (isQueryWithKnownAnswer) {
+                return MdnsUtils.createQueryDatagramPackets(packetCreationBuffer, packet, address);
+            } else {
+                final byte[] queryBuffer =
+                        MdnsUtils.createRawDnsPacket(packetCreationBuffer, packet);
+                return List.of(new DatagramPacket(queryBuffer, 0, queryBuffer.length, address));
+            }
+        }
     }
 
     /**
@@ -236,9 +264,10 @@
             @NonNull SocketKey socketKey,
             @NonNull SharedLog sharedLog,
             @NonNull Looper looper,
-            @NonNull MdnsServiceCache serviceCache) {
+            @NonNull MdnsServiceCache serviceCache,
+            @NonNull MdnsFeatureFlags featureFlags) {
         this(serviceType, socketClient, executor, new Clock(), socketKey, sharedLog, looper,
-                new Dependencies(), serviceCache);
+                new Dependencies(), serviceCache, featureFlags);
     }
 
     @VisibleForTesting
@@ -251,7 +280,8 @@
             @NonNull SharedLog sharedLog,
             @NonNull Looper looper,
             @NonNull Dependencies dependencies,
-            @NonNull MdnsServiceCache serviceCache) {
+            @NonNull MdnsServiceCache serviceCache,
+            @NonNull MdnsFeatureFlags featureFlags) {
         this.serviceType = serviceType;
         this.socketClient = socketClient;
         this.executor = executor;
@@ -265,6 +295,7 @@
         this.serviceCache = serviceCache;
         this.mdnsQueryScheduler = new MdnsQueryScheduler();
         this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
+        this.featureFlags = featureFlags;
     }
 
     /**
@@ -276,8 +307,8 @@
         serviceCache.unregisterServiceExpiredCallback(cacheKey);
     }
 
-    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
-            @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(@NonNull MdnsResponse response,
+            @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis) {
         String[] hostName = null;
         int port = 0;
         if (response.hasServiceRecord()) {
@@ -324,7 +355,12 @@
                 textEntries,
                 response.getInterfaceIndex(),
                 response.getNetwork(),
-                now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
+                now.plusMillis(response.getMinRemainingTtl(elapsedRealtimeMillis)));
+    }
+
+    private List<MdnsResponse> getExistingServices() {
+        return featureFlags.isQueryWithKnownAnswerEnabled()
+                ? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
     }
 
     /**
@@ -348,8 +384,8 @@
         if (existingInfo == null) {
             for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
                 if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
-                final MdnsServiceInfo info =
-                        buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
+                final MdnsServiceInfo info = buildMdnsServiceInfoFromResponse(
+                        existingResponse, serviceTypeLabels, clock.elapsedRealtime());
                 listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
                 listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
                 if (existingResponse.isComplete()) {
@@ -364,10 +400,7 @@
         // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
         // interested anymore.
         final QueryTaskConfig taskConfig = new QueryTaskConfig(
-                searchOptions.getQueryMode(),
-                searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
-                searchOptions.numOfQueriesBeforeBackoff(),
-                socketKey);
+                searchOptions.getQueryMode());
         final long now = clock.elapsedRealtime();
         if (lastSentTime == 0) {
             lastSentTime = now;
@@ -380,7 +413,9 @@
                             minRemainingTtl,
                             now,
                             lastSentTime,
-                            currentSessionId
+                            currentSessionId,
+                            searchOptions.getQueryMode(),
+                            searchOptions.numOfQueriesBeforeBackoff()
                     );
             dependencies.sendMessageDelayed(
                     handler,
@@ -391,7 +426,9 @@
             final QueryTask queryTask = new QueryTask(
                     mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
                             minRemainingTtl, currentSessionId), servicesToResolve,
-                    getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
+                    getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+                    getExistingServices(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
+                    socketKey);
             executor.submit(queryTask);
         }
 
@@ -425,7 +462,7 @@
             @NonNull MdnsSearchOptions options) {
         final boolean matchesInstanceName = options.getResolveInstanceName() == null
                 // DNS is case-insensitive, so ignore case in the comparison
-                || MdnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
+                || DnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
                 response.getServiceInstanceName());
 
         // If discovery is requiring some subtypes, the response must have one that matches a
@@ -435,7 +472,7 @@
         final boolean matchesSubtype = options.getSubtypes().size() == 0
                 || CollectionUtils.any(options.getSubtypes(), requiredSub ->
                 CollectionUtils.any(responseSubtypes, actualSub ->
-                        MdnsUtils.equalsIgnoreDnsCase(
+                        DnsUtils.equalsIgnoreDnsCase(
                                 MdnsConstants.SUBTYPE_PREFIX + requiredSub, actualSub)));
 
         return matchesInstanceName && matchesSubtype;
@@ -502,7 +539,8 @@
             final long minRemainingTtl = getMinRemainingTtl(now);
             MdnsQueryScheduler.ScheduledQueryTaskArgs args =
                     mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
-                            lastSentTime, currentSessionId + 1);
+                            lastSentTime, currentSessionId + 1,
+                            searchOptions.numOfQueriesBeforeBackoff());
             if (args != null) {
                 removeScheduledTask();
                 dependencies.sendMessageDelayed(
@@ -528,7 +566,7 @@
             if (response.getServiceInstanceName() != null) {
                 listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName());
                 final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
-                        response, serviceTypeLabels);
+                        response, serviceTypeLabels, clock.elapsedRealtime());
                 if (response.isComplete()) {
                     sharedLog.log(message + ". onServiceRemoved: " + serviceInfo);
                     listener.onServiceRemoved(serviceInfo);
@@ -572,8 +610,8 @@
                         + " %b, responseIsComplete: %b",
                 serviceInstanceName, newInCache, serviceBecomesComplete,
                 response.isComplete()));
-        MdnsServiceInfo serviceInfo =
-                buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+        final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+                response, serviceTypeLabels, clock.elapsedRealtime());
 
         for (int i = 0; i < listeners.size(); i++) {
             // If a service stops matching the options (currently can only happen if it loses a
@@ -617,11 +655,6 @@
         return searchOptions != null && searchOptions.removeExpiredService();
     }
 
-    @VisibleForTesting
-    MdnsPacketWriter createMdnsPacketWriter() {
-        return new MdnsPacketWriter(DEFAULT_MTU);
-    }
-
     private List<MdnsResponse> makeResponsesForResolve(@NonNull SocketKey socketKey) {
         final List<MdnsResponse> resolveResponses = new ArrayList<>();
         for (int i = 0; i < listeners.size(); i++) {
@@ -630,7 +663,7 @@
                 continue;
             }
             if (CollectionUtils.any(resolveResponses,
-                    r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+                    r -> DnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
                 continue;
             }
             MdnsResponse knownResponse =
@@ -694,14 +727,22 @@
         private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
         private final List<String> subtypes = new ArrayList<>();
         private final boolean sendDiscoveryQueries;
+        private final List<MdnsResponse> existingServices = new ArrayList<>();
+        private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
+        private final SocketKey socketKey;
         QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
                 @NonNull Collection<MdnsResponse> servicesToResolve,
-                @NonNull Collection<String> subtypes,
-                boolean sendDiscoveryQueries) {
+                @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
+                @NonNull Collection<MdnsResponse> existingServices,
+                boolean onlyUseIpv6OnIpv6OnlyNetworks,
+                @NonNull SocketKey socketKey) {
             this.taskArgs = taskArgs;
             this.servicesToResolve.addAll(servicesToResolve);
             this.subtypes.addAll(subtypes);
             this.sendDiscoveryQueries = sendDiscoveryQueries;
+            this.existingServices.addAll(existingServices);
+            this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
+            this.socketKey = socketKey;
         }
 
         @Override
@@ -711,17 +752,19 @@
                 result =
                         new EnqueueMdnsQueryCallable(
                                 socketClient,
-                                createMdnsPacketWriter(),
                                 serviceType,
                                 subtypes,
                                 taskArgs.config.expectUnicastResponse,
                                 taskArgs.config.transactionId,
-                                taskArgs.config.socketKey,
-                                taskArgs.config.onlyUseIpv6OnIpv6OnlyNetworks,
+                                socketKey,
+                                onlyUseIpv6OnIpv6OnlyNetworks,
                                 sendDiscoveryQueries,
                                 servicesToResolve,
                                 clock,
-                                sharedLog)
+                                sharedLog,
+                                dependencies,
+                                existingServices,
+                                featureFlags.isQueryWithKnownAnswerEnabled())
                                 .call();
             } catch (RuntimeException e) {
                 sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
index c51811b..653ea6c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
@@ -58,7 +58,6 @@
     MdnsSocket(@NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider,
             MulticastSocket multicastSocket, SharedLog sharedLog) throws IOException {
         this.multicastNetworkInterfaceProvider = multicastNetworkInterfaceProvider;
-        this.multicastNetworkInterfaceProvider.startWatchingConnectivityChanges();
         this.multicastSocket = multicastSocket;
         this.sharedLog = sharedLog;
         // RFC Spec: https://tools.ietf.org/html/rfc6762
@@ -120,7 +119,6 @@
     public void close() {
         // This is a race with the use of the file descriptor (b/27403984).
         multicastSocket.close();
-        multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
     }
 
     /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index 82c8c5b..17e5b31 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -25,9 +25,12 @@
 import android.net.wifi.WifiManager.MulticastLock;
 import android.os.SystemClock;
 import android.text.format.DateUtils;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -106,6 +109,7 @@
     @Nullable private Timer checkMulticastResponseTimer;
     private final SharedLog sharedLog;
     @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
+    private final MulticastNetworkInterfaceProvider interfaceProvider;
 
     public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock,
             SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
@@ -118,6 +122,7 @@
             unicastReceiverBuffer = null;
         }
         this.mdnsFeatureFlags = mdnsFeatureFlags;
+        this.interfaceProvider = new MulticastNetworkInterfaceProvider(context, sharedLog);
     }
 
     @Override
@@ -138,6 +143,7 @@
         cannotReceiveMulticastResponse.set(false);
 
         shouldStopSocketLoop = false;
+        interfaceProvider.startWatchingConnectivityChanges();
         try {
             // TODO (changed when importing code): consider setting thread stats tag
             multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT, sharedLog);
@@ -183,6 +189,7 @@
         }
 
         multicastLock.release();
+        interfaceProvider.stopWatchingConnectivityChanges();
 
         shouldStopSocketLoop = true;
         waitForSendThreadToStop();
@@ -202,18 +209,18 @@
     }
 
     @Override
-    public void sendPacketRequestingMulticastResponse(@NonNull DatagramPacket packet,
+    public void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
             boolean onlyUseIpv6OnIpv6OnlyNetworks) {
-        sendMdnsPacket(packet, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
+        sendMdnsPackets(packets, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
     }
 
     @Override
-    public void sendPacketRequestingUnicastResponse(@NonNull DatagramPacket packet,
+    public void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
             boolean onlyUseIpv6OnIpv6OnlyNetworks) {
         if (useSeparateSocketForUnicast) {
-            sendMdnsPacket(packet, unicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
+            sendMdnsPackets(packets, unicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
         } else {
-            sendMdnsPacket(packet, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
+            sendMdnsPackets(packets, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
         }
     }
 
@@ -234,17 +241,27 @@
         return false;
     }
 
-    private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse,
-            boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+    private void sendMdnsPackets(List<DatagramPacket> packets,
+            Queue<DatagramPacket> packetQueueToUse, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
         if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
             sharedLog.w("sendMdnsPacket() is called after discovery already stopped");
             return;
         }
+        if (packets.isEmpty()) {
+            Log.wtf(TAG, "No mDns packets to send");
+            return;
+        }
+        // Check all packets with the same address
+        if (!MdnsUtils.checkAllPacketsWithSameAddress(packets)) {
+            Log.wtf(TAG, "Some mDNS packets have a different target address. addresses="
+                    + CollectionUtils.map(packets, DatagramPacket::getSocketAddress));
+            return;
+        }
 
-        final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
-                instanceof Inet4Address;
-        final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
-                instanceof Inet6Address;
+        final boolean isIpv4 = ((InetSocketAddress) packets.get(0).getSocketAddress())
+                .getAddress() instanceof Inet4Address;
+        final boolean isIpv6 = ((InetSocketAddress) packets.get(0).getSocketAddress())
+                .getAddress() instanceof Inet6Address;
         final boolean ipv6Only = multicastSocket != null && multicastSocket.isOnIPv6OnlyNetwork();
         if (isIpv4 && ipv6Only) {
             return;
@@ -254,10 +271,11 @@
         }
 
         synchronized (packetQueueToUse) {
-            while (packetQueueToUse.size() >= MdnsConfigs.mdnsPacketQueueMaxSize()) {
+            while ((packetQueueToUse.size() + packets.size())
+                    > MdnsConfigs.mdnsPacketQueueMaxSize()) {
                 packetQueueToUse.remove();
             }
-            packetQueueToUse.add(packet);
+            packetQueueToUse.addAll(packets);
         }
         triggerSendThread();
     }
@@ -482,8 +500,7 @@
 
     @VisibleForTesting
     MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) throws IOException {
-        return new MdnsSocket(new MulticastNetworkInterfaceProvider(context, sharedLog), port,
-                sharedLog);
+        return new MdnsSocket(interfaceProvider, port, sharedLog);
     }
 
     private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
index b6000f0..b1a543a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
@@ -23,6 +23,7 @@
 
 import java.io.IOException;
 import java.net.DatagramPacket;
+import java.util.List;
 
 /**
  * Base class for multicast socket client.
@@ -40,15 +41,15 @@
     void setCallback(@Nullable Callback callback);
 
     /**
-     * Send a mDNS request packet via given network that asks for multicast response.
+     * Send mDNS request packets via given network that asks for multicast response.
      */
-    void sendPacketRequestingMulticastResponse(@NonNull DatagramPacket packet,
+    void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
             boolean onlyUseIpv6OnIpv6OnlyNetworks);
 
     /**
-     * Send a mDNS request packet via given network that asks for unicast response.
+     * Send mDNS request packets via given network that asks for unicast response.
      */
-    void sendPacketRequestingUnicastResponse(@NonNull DatagramPacket packet,
+    void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
             boolean onlyUseIpv6OnIpv6OnlyNetworks);
 
     /*** Notify that the given network is requested for mdns discovery / resolution */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
index 92cf324..77d1d7a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -89,6 +89,13 @@
         }
     }
 
+    private boolean isEmpty() {
+        return entries == null || entries.size() == 0
+                // RFC6763 6.1 indicates that a TXT record with a single zero byte is equivalent to
+                // an empty record.
+                || (entries.size() == 1 && entries.get(0).isEmpty());
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -105,7 +112,7 @@
 
     @Override
     public int hashCode() {
-        return (super.hashCode() * 31) + Objects.hash(entries);
+        return (super.hashCode() * 31) + (isEmpty() ? 0 : Objects.hash(entries));
     }
 
     @Override
@@ -116,7 +123,19 @@
         if (!(other instanceof MdnsTextRecord)) {
             return false;
         }
-
-        return super.equals(other) && Objects.equals(entries, ((MdnsTextRecord) other).entries);
+        if (!super.equals(other)) {
+            return false;
+        }
+        // As per RFC6763 6.1: DNS-SD clients MUST treat the following as equivalent:
+        // - A TXT record containing a single zero byte.
+        // - An empty (zero-length) TXT record. (This is not strictly legal, but should one be
+        //   received, it should be interpreted as the same as a single empty string.)
+        // - No TXT record
+        // Ensure that empty TXT records are considered equal, so that they are not considered
+        // conflicting for example.
+        if (isEmpty() && ((MdnsTextRecord) other).isEmpty()) {
+            return true;
+        }
+        return Objects.equals(entries, ((MdnsTextRecord) other).entries);
     }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index 0894166..d2cd463 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -19,9 +19,6 @@
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
 import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
 import com.android.internal.annotations.VisibleForTesting;
 
 /**
@@ -51,9 +48,6 @@
     static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
     private final boolean alwaysAskForUnicastResponse =
             MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
-    private final int queryMode;
-    final boolean onlyUseIpv6OnIpv6OnlyNetworks;
-    private final int numOfQueriesBeforeBackoff;
     @VisibleForTesting
     final int transactionId;
     @VisibleForTesting
@@ -64,16 +58,11 @@
     final long delayUntilNextTaskWithoutBackoffMs;
     private final boolean isFirstBurst;
     private final long queryCount;
-    @NonNull
-    final SocketKey socketKey;
 
-    QueryTaskConfig(@NonNull QueryTaskConfig other, long queryCount, int transactionId,
+    QueryTaskConfig(long queryCount, int transactionId,
             boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
             int queriesPerBurst, int timeBetweenBurstsInMs,
             long delayUntilNextTaskWithoutBackoffMs) {
-        this.queryMode = other.queryMode;
-        this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
-        this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
         this.transactionId = transactionId;
         this.expectUnicastResponse = expectUnicastResponse;
         this.queriesPerBurst = queriesPerBurst;
@@ -82,27 +71,20 @@
         this.delayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
         this.isFirstBurst = isFirstBurst;
         this.queryCount = queryCount;
-        this.socketKey = other.socketKey;
     }
 
-    QueryTaskConfig(int queryMode,
-            boolean onlyUseIpv6OnIpv6OnlyNetworks,
-            int numOfQueriesBeforeBackoff,
-            @Nullable SocketKey socketKey) {
-        this.queryMode = queryMode;
-        this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
-        this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
+    QueryTaskConfig(int queryMode) {
         this.queriesPerBurst = QUERIES_PER_BURST;
         this.burstCounter = 0;
         this.transactionId = 1;
         this.expectUnicastResponse = true;
         this.isFirstBurst = true;
         // Config the scan frequency based on the scan mode.
-        if (this.queryMode == AGGRESSIVE_QUERY_MODE) {
+        if (queryMode == AGGRESSIVE_QUERY_MODE) {
             this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
             this.delayUntilNextTaskWithoutBackoffMs =
                     TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
-        } else if (this.queryMode == PASSIVE_QUERY_MODE) {
+        } else if (queryMode == PASSIVE_QUERY_MODE) {
             // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
             // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
             // queries.
@@ -116,12 +98,11 @@
             this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
             this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         }
-        this.socketKey = socketKey;
         this.queryCount = 0;
     }
 
     long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
-            boolean isLastQueryInBurst) {
+            boolean isLastQueryInBurst, int queryMode) {
         if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
             return 0;
         }
@@ -133,7 +114,7 @@
                 : TIME_BETWEEN_QUERIES_IN_BURST_MS;
     }
 
-    boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst) {
+    boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst, int queryMode) {
         if (!isLastQueryInBurst) {
             return false;
         }
@@ -143,7 +124,7 @@
         return alwaysAskForUnicastResponse;
     }
 
-    int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst) {
+    int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst, int queryMode) {
         if (!isLastQueryInBurst) {
             return timeBetweenBurstsInMs;
         }
@@ -155,7 +136,7 @@
     /**
      * Get new QueryTaskConfig for next run.
      */
-    public QueryTaskConfig getConfigForNextRun() {
+    public QueryTaskConfig getConfigForNextRun(int queryMode) {
         long newQueryCount = queryCount + 1;
         int newTransactionId = transactionId + 1;
         if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
@@ -177,16 +158,18 @@
             }
         }
 
-        return new QueryTaskConfig(this, newQueryCount, newTransactionId,
-                getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter,
-                newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst),
-                getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst));
+        return new QueryTaskConfig(newQueryCount, newTransactionId,
+                getNextExpectUnicastResponse(isLastQueryInBurst, queryMode), newIsFirstBurst,
+                newBurstCounter, newQueriesPerBurst,
+                getNextTimeBetweenBurstsMs(isLastQueryInBurst, queryMode),
+                getDelayUntilNextTaskWithoutBackoff(
+                        isFirstQueryInBurst, isLastQueryInBurst, queryMode));
     }
 
     /**
      * Determine if the query backoff should be used.
      */
-    public boolean shouldUseQueryBackoff() {
+    public boolean shouldUseQueryBackoff(int numOfQueriesBeforeBackoff) {
         // Don't enable backoff mode during the burst or in the first burst
         if (burstCounter != 0 || isFirstBurst) {
             return false;
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index d553210..8745941 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -16,6 +16,10 @@
 
 package com.android.server.connectivity.mdns.util;
 
+import static com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase;
+import static com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase;
+import static com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.Network;
@@ -23,6 +27,7 @@
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.ArraySet;
+import android.util.Pair;
 
 import com.android.server.connectivity.mdns.MdnsConstants;
 import com.android.server.connectivity.mdns.MdnsPacket;
@@ -30,13 +35,19 @@
 import com.android.server.connectivity.mdns.MdnsRecord;
 
 import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetEncoder;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -47,18 +58,17 @@
     private MdnsUtils() { }
 
     /**
-     * Convert the string to DNS case-insensitive lowercase
+     * Compare labels a equals b or a is suffix of b.
      *
-     * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
-     * their unaccented counterparts. So the "DNS lowercase" should be if character is A-Z then they
-     * transform into a-z. Otherwise, they are kept as-is.
+     * @param a the type or subtype.
+     * @param b the base type
      */
-    public static String toDnsLowerCase(@NonNull String string) {
-        final char[] outChars = new char[string.length()];
-        for (int i = 0; i < string.length(); i++) {
-            outChars[i] = toDnsLowerCase(string.charAt(i));
-        }
-        return new String(outChars);
+    public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
+            @NonNull String[] b) {
+        return equalsDnsLabelIgnoreDnsCase(a, b)
+                || ((b.length == (a.length + 2))
+                && equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
+                && MdnsRecord.labelsAreSuffix(a, b));
     }
 
     /**
@@ -72,70 +82,6 @@
         }
     }
 
-    /**
-     * Convert the array of labels to DNS case-insensitive lowercase.
-     */
-    public static String[] toDnsLabelsLowerCase(@NonNull String[] labels) {
-        final String[] outStrings = new String[labels.length];
-        for (int i = 0; i < labels.length; ++i) {
-            outStrings[i] = toDnsLowerCase(labels[i]);
-        }
-        return outStrings;
-    }
-
-    /**
-     * Compare two strings by DNS case-insensitive lowercase.
-     */
-    public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
-        if (a == null || b == null) {
-            return a == null && b == null;
-        }
-        if (a.length() != b.length()) return false;
-        for (int i = 0; i < a.length(); i++) {
-            if (toDnsLowerCase(a.charAt(i)) != toDnsLowerCase(b.charAt(i))) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Compare two set of DNS labels by DNS case-insensitive lowercase.
-     */
-    public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
-        if (a == b) {
-            return true;
-        }
-        int length = a.length;
-        if (b.length != length) {
-            return false;
-        }
-        for (int i = 0; i < length; i++) {
-            if (!equalsIgnoreDnsCase(a[i], b[i])) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Compare labels a equals b or a is suffix of b.
-     *
-     * @param a the type or subtype.
-     * @param b the base type
-     */
-    public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
-            @NonNull String[] b) {
-        return MdnsUtils.equalsDnsLabelIgnoreDnsCase(a, b)
-                || ((b.length == (a.length + 2))
-                && MdnsUtils.equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
-                && MdnsRecord.labelsAreSuffix(a, b));
-    }
-
-    private static char toDnsLowerCase(char a) {
-        return a >= 'A' && a <= 'Z' ? (char) (a + ('a' - 'A')) : a;
-    }
-
     /*** Ensure that current running thread is same as given handler thread */
     public static void ensureRunningOnHandlerThread(@NonNull Handler handler) {
         if (!isRunningOnHandlerThread(handler)) {
@@ -226,6 +172,100 @@
     }
 
     /**
+     * Writes the possible query content of an MdnsPacket into the data buffer.
+     *
+     * <p>This method is specifically for query packets. It writes the question and answer sections
+     *    into the data buffer only.
+     *
+     * @param packetCreationBuffer The data buffer for the query content.
+     * @param packet The MdnsPacket to be written into the data buffer.
+     * @return A Pair containing:
+     *         1. The remaining MdnsPacket data that could not fit in the buffer.
+     *         2. The length of the data written to the buffer.
+     */
+    @Nullable
+    private static Pair<MdnsPacket, Integer> writePossibleMdnsPacket(
+            @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet) throws IOException {
+        MdnsPacket remainingPacket;
+        final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
+        writer.writeUInt16(packet.transactionId); // Transaction ID
+
+        final int flagsPos = writer.getWritePosition();
+        writer.writeUInt16(0); // Flags, written later
+        writer.writeUInt16(0); // questions count, written later
+        writer.writeUInt16(0); // answers count, written later
+        writer.writeUInt16(0); // authority entries count, empty session for query
+        writer.writeUInt16(0); // additional records count, empty session for query
+
+        int writtenQuestions = 0;
+        int writtenAnswers = 0;
+        int lastValidPos = writer.getWritePosition();
+        try {
+            for (MdnsRecord record : packet.questions) {
+                // Questions do not have TTL or data
+                record.writeHeaderFields(writer);
+                writtenQuestions++;
+                lastValidPos = writer.getWritePosition();
+            }
+            for (MdnsRecord record : packet.answers) {
+                record.write(writer, 0L);
+                writtenAnswers++;
+                lastValidPos = writer.getWritePosition();
+            }
+            remainingPacket = null;
+        } catch (IOException e) {
+            // Went over the packet limit; truncate
+            if (writtenQuestions == 0 && writtenAnswers == 0) {
+                // No space to write even one record: just throw (as subclass of IOException)
+                throw e;
+            }
+
+            // Set the last valid position as the final position (not as a rewind)
+            writer.rewind(lastValidPos);
+            writer.clearRewind();
+
+            remainingPacket = new MdnsPacket(packet.flags,
+                    packet.questions.subList(
+                            writtenQuestions, packet.questions.size()),
+                    packet.answers.subList(
+                            writtenAnswers, packet.answers.size()),
+                    Collections.emptyList(), /* authorityRecords */
+                    Collections.emptyList() /* additionalRecords */);
+        }
+
+        final int len = writer.getWritePosition();
+        writer.rewind(flagsPos);
+        writer.writeUInt16(packet.flags | (remainingPacket == null ? 0 : FLAG_TRUNCATED));
+        writer.writeUInt16(writtenQuestions);
+        writer.writeUInt16(writtenAnswers);
+        writer.unrewind();
+
+        return Pair.create(remainingPacket, len);
+    }
+
+    /**
+     * Create Datagram packets from given MdnsPacket and InetSocketAddress.
+     *
+     * <p> If the MdnsPacket is too large for a single DatagramPacket, it will be split into
+     *     multiple DatagramPackets.
+     */
+    public static List<DatagramPacket> createQueryDatagramPackets(
+            @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet,
+            @NonNull InetSocketAddress destination) throws IOException {
+        final List<DatagramPacket> datagramPackets = new ArrayList<>();
+        MdnsPacket remainingPacket = packet;
+        while (remainingPacket != null) {
+            final Pair<MdnsPacket, Integer> result =
+                    writePossibleMdnsPacket(packetCreationBuffer, remainingPacket);
+            remainingPacket = result.first;
+            final int len = result.second;
+            final byte[] outBuffer = Arrays.copyOfRange(packetCreationBuffer, 0, len);
+            datagramPackets.add(new DatagramPacket(outBuffer, 0, outBuffer.length, destination));
+        }
+        return datagramPackets;
+    }
+
+    /**
      * Checks if the MdnsRecord needs to be renewed or not.
      *
      * <p>As per RFC6762 7.1 no need to query if remaining TTL is more than half the original one,
@@ -259,4 +299,23 @@
             return SystemClock.elapsedRealtime();
         }
     }
+
+    /**
+     * Check all DatagramPackets with the same destination address.
+     */
+    public static boolean checkAllPacketsWithSameAddress(List<DatagramPacket> packets) {
+        // No packet for address check
+        if (packets.isEmpty()) {
+            return true;
+        }
+
+        final InetAddress address =
+                ((InetSocketAddress) packets.get(0).getSocketAddress()).getAddress();
+        for (DatagramPacket packet : packets) {
+            if (!address.equals(((InetSocketAddress) packet.getSocketAddress()).getAddress())) {
+                return false;
+            }
+        }
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java b/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
new file mode 100644
index 0000000..b8f6859
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Message;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.util.State;
+import com.android.net.module.util.SyncStateMachine;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * EthernetInterfaceStateMachine manages the lifecycle of an ethernet-like network interface which
+ * includes managing a NetworkOffer, IpClient, and NetworkAgent as well as making the interface
+ * available as a tethering downstream.
+ *
+ * All methods exposed by this class *must* be called on the Handler thread provided in the
+ * constructor.
+ */
+class EthernetInterfaceStateMachine extends SyncStateMachine {
+    private static final String TAG = EthernetInterfaceStateMachine.class.getSimpleName();
+
+    private static final int CMD_ON_LINK_UP          = 1;
+    private static final int CMD_ON_LINK_DOWN        = 2;
+    private static final int CMD_ON_NETWORK_NEEDED   = 3;
+    private static final int CMD_ON_NETWORK_UNNEEDED = 4;
+    private static final int CMD_ON_IPCLIENT_CREATED = 5;
+
+    private class EthernetNetworkOfferCallback implements NetworkOfferCallback {
+        private final Set<Integer> mRequestIds = new ArraySet<>();
+
+        @Override
+        public void onNetworkNeeded(@NonNull NetworkRequest request) {
+            if (this != mNetworkOfferCallback) {
+                return;
+            }
+
+            mRequestIds.add(request.requestId);
+            if (mRequestIds.size() == 1) {
+                processMessage(CMD_ON_NETWORK_NEEDED);
+            }
+        }
+
+        @Override
+        public void onNetworkUnneeded(@NonNull NetworkRequest request) {
+            if (this != mNetworkOfferCallback) {
+                return;
+            }
+
+            if (!mRequestIds.remove(request.requestId)) {
+                // This can only happen if onNetworkNeeded was not called for a request or if
+                // the requestId changed. Both should *never* happen.
+                Log.wtf(TAG, "onNetworkUnneeded called for unknown request");
+            }
+            if (mRequestIds.isEmpty()) {
+                processMessage(CMD_ON_NETWORK_UNNEEDED);
+            }
+        }
+    }
+
+    private class EthernetIpClientCallback extends IpClientCallbacks {
+        private final ConditionVariable mOnQuitCv = new ConditionVariable(false);
+
+        private void safelyPostOnHandler(Runnable r) {
+            mHandler.post(() -> {
+                if (this != mIpClientCallback) {
+                    return;
+                }
+                r.run();
+            });
+        }
+
+        @Override
+        public void onIpClientCreated(IIpClient ipClient) {
+            safelyPostOnHandler(() -> {
+                // TODO: add a SyncStateMachine#processMessage(cmd, obj) overload.
+                processMessage(CMD_ON_IPCLIENT_CREATED, 0, 0,
+                        mDependencies.makeIpClientManager(ipClient));
+            });
+        }
+
+        public void waitOnQuit() {
+            if (!mOnQuitCv.block(5_000 /* timeoutMs */)) {
+                Log.wtf(TAG, "Timed out waiting on IpClient to shutdown.");
+            }
+        }
+
+        @Override
+        public void onQuit() {
+            mOnQuitCv.open();
+        }
+    }
+
+    private @Nullable EthernetNetworkOfferCallback mNetworkOfferCallback;
+    private @Nullable EthernetIpClientCallback mIpClientCallback;
+    private @Nullable IpClientManager mIpClient;
+    private final String mIface;
+    private final Handler mHandler;
+    private final Context mContext;
+    private final NetworkCapabilities mCapabilities;
+    private final NetworkProvider mNetworkProvider;
+    private final EthernetNetworkFactory.Dependencies mDependencies;
+    private boolean mLinkUp = false;
+
+    /** Interface is in tethering mode. */
+    private class TetheringState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_ON_LINK_UP:
+                case CMD_ON_LINK_DOWN:
+                    // TODO: think about what to do here.
+                    return HANDLED;
+            }
+            return NOT_HANDLED;
+        }
+    }
+
+    /** Link is down */
+    private class LinkDownState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_ON_LINK_UP:
+                    transitionTo(mStoppedState);
+                    return HANDLED;
+                case CMD_ON_LINK_DOWN:
+                    // do nothing, already in the correct state.
+                    return HANDLED;
+            }
+            return NOT_HANDLED;
+        }
+    }
+
+    /** Parent states of all states that do not cause a NetworkOffer to be extended. */
+    private class NetworkOfferExtendedState extends State {
+        @Override
+        public void enter() {
+            if (mNetworkOfferCallback != null) {
+                // This should never happen. If it happens anyway, log and move on.
+                Log.wtf(TAG, "Previous NetworkOffer was never retracted");
+            }
+
+            mNetworkOfferCallback = new EthernetNetworkOfferCallback();
+            final NetworkScore defaultScore = new NetworkScore.Builder().build();
+            mNetworkProvider.registerNetworkOffer(defaultScore,
+                    new NetworkCapabilities(mCapabilities), cmd -> mHandler.post(cmd),
+                    mNetworkOfferCallback);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_ON_LINK_UP:
+                    // do nothing, already in the correct state.
+                    return HANDLED;
+                case CMD_ON_LINK_DOWN:
+                    transitionTo(mLinkDownState);
+                    return HANDLED;
+            }
+            return NOT_HANDLED;
+        }
+
+        @Override
+        public void exit() {
+            mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
+            mNetworkOfferCallback = null;
+        }
+    }
+
+    /**
+     * Offer is extended but has not been requested.
+     *
+     * StoppedState's sole purpose is to react to a CMD_ON_NETWORK_NEEDED and transition to
+     * StartedState when that happens. Note that StoppedState could be rolled into
+     * NetworkOfferExtendedState. However, keeping the states separate provides some additional
+     * protection by logging a Log.wtf if a CMD_ON_NETWORK_NEEDED is received in an unexpected state
+     * (i.e. StartedState or RunningState). StoppedState is a child of NetworkOfferExtendedState.
+     */
+    private class StoppedState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_ON_NETWORK_NEEDED:
+                    transitionTo(mStartedState);
+                    return HANDLED;
+            }
+            return NOT_HANDLED;
+        }
+    }
+
+    /** Network is needed, starts IpClient and manages its lifecycle */
+    private class StartedState extends State {
+        @Override
+        public void enter() {
+            mIpClientCallback = new EthernetIpClientCallback();
+            mDependencies.makeIpClient(mContext, mIface, mIpClientCallback);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case CMD_ON_NETWORK_UNNEEDED:
+                    transitionTo(mStoppedState);
+                    return HANDLED;
+                case CMD_ON_IPCLIENT_CREATED:
+                    mIpClient = (IpClientManager) msg.obj;
+                    transitionTo(mRunningState);
+                    return HANDLED;
+            }
+            return NOT_HANDLED;
+        }
+
+        @Override
+        public void exit() {
+            if (mIpClient != null) {
+                mIpClient.shutdown();
+                // TODO: consider adding a StoppingState and making the shutdown operation
+                // asynchronous.
+                mIpClientCallback.waitOnQuit();
+            }
+            mIpClientCallback = null;
+        }
+    }
+
+    /** IpClient is running, starts provisioning and registers NetworkAgent */
+    private class RunningState extends State {
+
+    }
+
+    private final TetheringState mTetheringState = new TetheringState();
+    private final LinkDownState mLinkDownState = new LinkDownState();
+    private final NetworkOfferExtendedState mOfferExtendedState = new NetworkOfferExtendedState();
+    private final StoppedState mStoppedState = new StoppedState();
+    private final StartedState mStartedState = new StartedState();
+    private final RunningState mRunningState = new RunningState();
+
+    public EthernetInterfaceStateMachine(String iface, Handler handler, Context context,
+            NetworkCapabilities capabilities, NetworkProvider provider,
+            EthernetNetworkFactory.Dependencies deps) {
+        super(TAG + "." + iface, handler.getLooper().getThread());
+
+        mIface = iface;
+        mHandler = handler;
+        mContext = context;
+        mCapabilities = capabilities;
+        mNetworkProvider = provider;
+        mDependencies = deps;
+
+        // Interface lifecycle:
+        //           [ LinkDownState ]
+        //                   |
+        //                   v
+        //             *link comes up*
+        //                   |
+        //                   v
+        //            [ StoppedState ]
+        //                   |
+        //                   v
+        //           *network is needed*
+        //                   |
+        //                   v
+        //            [ StartedState ]
+        //                   |
+        //                   v
+        //           *IpClient is created*
+        //                   |
+        //                   v
+        //            [ RunningState ]
+        //                   |
+        //                   v
+        //  *interface is requested for tethering*
+        //                   |
+        //                   v
+        //            [TetheringState]
+        //
+        // Tethering mode is special as the interface is configured by Tethering, rather than the
+        // ethernet module.
+        final List<StateInfo> states = new ArrayList<>();
+        states.add(new StateInfo(mTetheringState, null));
+
+        // CHECKSTYLE:OFF IndentationCheck
+        // Initial state
+        states.add(new StateInfo(mLinkDownState, null));
+        states.add(new StateInfo(mOfferExtendedState, null));
+            states.add(new StateInfo(mStoppedState, mOfferExtendedState));
+            states.add(new StateInfo(mStartedState, mOfferExtendedState));
+                states.add(new StateInfo(mRunningState, mStartedState));
+        // CHECKSTYLE:ON IndentationCheck
+        addAllStates(states);
+
+        // TODO: set initial state to TetheringState if a tethering interface has been requested and
+        // this is the first interface to be added.
+        start(mLinkDownState);
+    }
+
+    public boolean updateLinkState(boolean up) {
+        if (mLinkUp == up) {
+            return false;
+        }
+
+        // TODO: consider setting mLinkUp as part of processMessage().
+        mLinkUp = up;
+        if (!up) { // was up, goes down
+            processMessage(CMD_ON_LINK_DOWN);
+        } else { // was down, comes up
+            processMessage(CMD_ON_LINK_UP);
+        }
+
+        return true;
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 0b54fdd..cadc04d 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -250,6 +250,17 @@
         return mTrackingInterfaces.containsKey(ifaceName);
     }
 
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Nullable
+    protected String getHwAddress(@NonNull final String ifaceName) {
+        if (!hasInterface(ifaceName)) {
+            return null;
+        }
+
+        NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+        return iface.mHwAddress;
+    }
+
     @VisibleForTesting
     static class NetworkInterfaceState {
         final String name;
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 458d64f..71f289e 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -131,6 +131,10 @@
     // returned when a tethered interface is requested; until then, it remains in client mode. Its
     // current mode is reflected in mTetheringInterfaceMode.
     private String mTetheringInterface;
+    // If the tethering interface is in server mode, it is not tracked by factory. The HW address
+    // must be maintained by the EthernetTracker. Its current mode is reflected in
+    // mTetheringInterfaceMode.
+    private String mTetheringInterfaceHwAddr;
     private int mTetheringInterfaceMode = INTERFACE_MODE_CLIENT;
     // Tracks whether clients were notified that the tethered interface is available
     private boolean mTetheredInterfaceWasAvailable = false;
@@ -382,10 +386,9 @@
         });
     }
 
-    @VisibleForTesting(visibility = PACKAGE)
-    protected void setInterfaceEnabled(@NonNull final String iface, boolean enabled,
-            @Nullable final EthernetCallback cb) {
-        mHandler.post(() -> updateInterfaceState(iface, enabled, cb));
+    /** Configure the administrative state of ethernet interface by toggling IFF_UP. */
+    public void setInterfaceEnabled(String iface, boolean enabled, EthernetCallback cb) {
+        mHandler.post(() -> setInterfaceAdministrativeState(iface, enabled, cb));
     }
 
     IpConfiguration getIpConfiguration(String iface) {
@@ -461,7 +464,7 @@
             if (!include) {
                 removeTestData();
             }
-            mHandler.post(() -> trackAvailableInterfaces());
+            trackAvailableInterfaces();
         });
     }
 
@@ -583,6 +586,7 @@
         removeInterface(iface);
         if (iface.equals(mTetheringInterface)) {
             mTetheringInterface = null;
+            mTetheringInterfaceHwAddr = null;
         }
         broadcastInterfaceStateChange(iface);
     }
@@ -611,13 +615,14 @@
             return;
         }
 
+        final String hwAddress = config.hwAddr;
+
         if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
             maybeUpdateServerModeInterfaceState(iface, true);
+            mTetheringInterfaceHwAddr = hwAddress;
             return;
         }
 
-        final String hwAddress = config.hwAddr;
-
         NetworkCapabilities nc = mNetworkCapabilities.get(iface);
         if (nc == null) {
             // Try to resolve using mac address
@@ -643,25 +648,40 @@
         }
     }
 
-    private void updateInterfaceState(String iface, boolean up) {
-        updateInterfaceState(iface, up, new EthernetCallback(null /* cb */));
-    }
-
-    // TODO(b/225315248): enable/disableInterface() should not affect link state.
-    private void updateInterfaceState(String iface, boolean up, EthernetCallback cb) {
-        final int mode = getInterfaceMode(iface);
-        if (mode == INTERFACE_MODE_SERVER || !mFactory.hasInterface(iface)) {
-            // The interface is in server mode or is not tracked.
-            cb.onError("Failed to set link state " + (up ? "up" : "down") + " for " + iface);
+    private void setInterfaceAdministrativeState(String iface, boolean up, EthernetCallback cb) {
+        if (getInterfaceState(iface) == EthernetManager.STATE_ABSENT) {
+            cb.onError("Failed to enable/disable absent interface: " + iface);
+            return;
+        }
+        if (getInterfaceRole(iface) == EthernetManager.ROLE_SERVER) {
+            // TODO: support setEthernetState for server mode interfaces.
+            cb.onError("Failed to enable/disable interface in server mode: " + iface);
             return;
         }
 
+        if (up) {
+            // WARNING! setInterfaceUp() clears the IPv4 address and readds it. Calling
+            // enableInterface() on an active interface can lead to a provisioning failure which
+            // will cause IpClient to be restarted.
+            // TODO: use netlink directly rather than calling into netd.
+            NetdUtils.setInterfaceUp(mNetd, iface);
+        } else {
+            NetdUtils.setInterfaceDown(mNetd, iface);
+        }
+        cb.onResult(iface);
+    }
+
+    private void updateInterfaceState(String iface, boolean up) {
+        final int mode = getInterfaceMode(iface);
+        if (mode == INTERFACE_MODE_SERVER) {
+            // TODO: support tracking link state for interfaces in server mode.
+            return;
+        }
+
+        // If updateInterfaceLinkState returns false, the interface is already in the correct state.
         if (mFactory.updateInterfaceLinkState(iface, up)) {
             broadcastInterfaceStateChange(iface);
         }
-        // If updateInterfaceLinkState returns false, the interface is already in the correct state.
-        // Always return success.
-        cb.onResult(iface);
     }
 
     private void maybeUpdateServerModeInterfaceState(String iface, boolean available) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index f7f133c..11343d2 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.net;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.NETWORK_STATS_PROVIDER;
 import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
@@ -50,12 +51,18 @@
 import static android.net.NetworkTemplate.MATCH_WIFI;
 import static android.net.TrafficStats.KB_IN_BYTES;
 import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.net.TrafficStats.TYPE_RX_BYTES;
+import static android.net.TrafficStats.TYPE_RX_PACKETS;
+import static android.net.TrafficStats.TYPE_TX_BYTES;
+import static android.net.TrafficStats.TYPE_TX_PACKETS;
 import static android.net.TrafficStats.UID_TETHERING;
 import static android.net.TrafficStats.UNSUPPORTED;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
 import static android.os.Trace.TRACE_TAG_NETWORK;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 import static android.system.OsConstants.ENOENT;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 import static android.text.format.DateUtils.DAY_IN_MILLIS;
@@ -64,14 +71,15 @@
 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.net.module.util.DeviceConfigUtils.getDeviceConfigPropertyInt;
 import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
 import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
-import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
@@ -84,6 +92,7 @@
 import android.app.AlarmManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
+import android.app.compat.CompatChanges;
 import android.app.usage.NetworkStatsManager;
 import android.content.ApexEnvironment;
 import android.content.BroadcastReceiver;
@@ -233,13 +242,11 @@
     // A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent
     // deadlock.
     private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4;
-
     /** Flags to control detail level of poll event. */
     private static final int FLAG_PERSIST_NETWORK = 0x1;
     private static final int FLAG_PERSIST_UID = 0x2;
     private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID;
     private static final int FLAG_PERSIST_FORCE = 0x100;
-
     /**
      * When global alert quota is high, wait for this delay before processing each polling,
      * and do not schedule further polls once there is already one queued.
@@ -299,6 +306,18 @@
     static final String NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME = "fastdatainput.successes";
     static final String NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME = "fastdatainput.fallbacks";
 
+    static final String TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME =
+            "trafficstats_cache_expiry_duration_ms";
+    static final String TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME = "trafficstats_cache_max_entries";
+    static final int DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS = 1000;
+    static final int DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES = 400;
+    /**
+     * The delay time between to network stats update intents.
+     * Added to fix intent spams (b/3115462)
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    static final int BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS = 1000;
+
     private final Context mContext;
     private final NetworkStatsFactory mStatsFactory;
     private final AlarmManager mAlarmManager;
@@ -370,6 +389,7 @@
         long getXtPersistBytes(long def);
         long getUidPersistBytes(long def);
         long getUidTagPersistBytes(long def);
+        long getBroadcastNetworkStatsUpdateDelayMs();
     }
 
     private final Object mStatsLock = new Object();
@@ -454,6 +474,36 @@
 
     private long mLastStatsSessionPoll;
 
+    /**
+     * The timestamp of the most recent network stats broadcast.
+     *
+     * Note that this time could be in the past for completed broadcasts,
+     * or in the future for scheduled broadcasts.
+     *
+     * It is initialized to {@code Long.MIN_VALUE} to ensure that the first broadcast request
+     * is fulfilled immediately, regardless of the delay time.
+     *
+     * This value is used to enforce rate limiting on intents, preventing intent spam.
+     */
+    @GuardedBy("mStatsLock")
+    private long mLatestNetworkStatsUpdatedBroadcastScheduledTime = Long.MIN_VALUE;
+
+
+    private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
+    private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
+    private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
+    static final String TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG =
+            "trafficstats_rate_limit_cache_enabled_flag";
+    static final String BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG =
+            "broadcast_network_stats_updated_rate_limit_enabled_flag";
+    private final boolean mAlwaysUseTrafficStatsRateLimitCache;
+    private final int mTrafficStatsRateLimitCacheExpiryDuration;
+    private final int mTrafficStatsRateLimitCacheMaxEntries;
+
+    private final boolean mBroadcastNetworkStatsUpdatedRateLimitEnabled;
+
+
+
     private final Object mOpenSessionCallsLock = new Object();
 
     /**
@@ -643,6 +693,21 @@
             mEventLogger = null;
         }
 
+        mAlwaysUseTrafficStatsRateLimitCache =
+                mDeps.alwaysUseTrafficStatsRateLimitCache(mContext);
+        mBroadcastNetworkStatsUpdatedRateLimitEnabled =
+                mDeps.enabledBroadcastNetworkStatsUpdatedRateLimiting(mContext);
+        mTrafficStatsRateLimitCacheExpiryDuration =
+                mDeps.getTrafficStatsRateLimitCacheExpiryDuration();
+        mTrafficStatsRateLimitCacheMaxEntries =
+                mDeps.getTrafficStatsRateLimitCacheMaxEntries();
+        mTrafficStatsTotalCache = new TrafficStatsRateLimitCache(mClock,
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+        mTrafficStatsIfaceCache = new TrafficStatsRateLimitCache(mClock,
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+        mTrafficStatsUidCache = new TrafficStatsRateLimitCache(mClock,
+                mTrafficStatsRateLimitCacheExpiryDuration, mTrafficStatsRateLimitCacheMaxEntries);
+
         // TODO: Remove bpfNetMaps creation and always start SkDestroyListener
         // Following code is for the experiment to verify the SkDestroyListener refactoring. Based
         // on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or
@@ -659,6 +724,15 @@
     @VisibleForTesting
     public static class Dependencies {
         /**
+         * Get broadcast network stats updated delay time in ms
+         * @return
+         */
+        @NonNull
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
+        }
+
+        /**
          * Get legacy platform stats directory.
          */
         @NonNull
@@ -696,7 +770,7 @@
          * Get the count of import legacy target attempts.
          */
         public int getImportLegacyTargetAttempts() {
-            return DeviceConfigUtils.getDeviceConfigPropertyInt(
+            return getDeviceConfigPropertyInt(
                     DeviceConfig.NAMESPACE_TETHERING,
                     NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS,
                     DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS);
@@ -706,7 +780,7 @@
          * Get the count of using FastDataInput target attempts.
          */
         public int getUseFastDataInputTargetAttempts() {
-            return DeviceConfigUtils.getDeviceConfigPropertyInt(
+            return getDeviceConfigPropertyInt(
                     DeviceConfig.NAMESPACE_TETHERING,
                     NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS, 0);
         }
@@ -888,6 +962,94 @@
             return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
                     ctx, CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER);
         }
+
+        /**
+         * Get whether broadcast network stats update rate limiting is enabled.
+         *
+         * This method should only be called once in the constructor,
+         * to ensure that the code does not need to deal with flag values changing at runtime.
+         */
+        public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+            return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    ctx, BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG);
+        }
+
+        /**
+         * Get whether TrafficStats rate-limit cache is always applied.
+         *
+         * This method should only be called once in the constructor,
+         * to ensure that the code does not need to deal with flag values changing at runtime.
+         */
+        public boolean alwaysUseTrafficStatsRateLimitCache(@NonNull Context ctx) {
+            return SdkLevel.isAtLeastV() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    ctx, TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG);
+        }
+
+        /**
+         * Get TrafficStats rate-limit cache expiry.
+         *
+         * This method should only be called once in the constructor,
+         * to ensure that the code does not need to deal with flag values changing at runtime.
+         */
+        public int getTrafficStatsRateLimitCacheExpiryDuration() {
+            return getDeviceConfigPropertyInt(
+                    NAMESPACE_TETHERING, TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
+                    DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS);
+        }
+
+        /**
+         * Get TrafficStats rate-limit cache max entries.
+         *
+         * This method should only be called once in the constructor,
+         * to ensure that the code does not need to deal with flag values changing at runtime.
+         */
+        public int getTrafficStatsRateLimitCacheMaxEntries() {
+            return getDeviceConfigPropertyInt(
+                    NAMESPACE_TETHERING, TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME,
+                    DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES);
+        }
+
+        /**
+         * Wrapper method for {@link CompatChanges#isChangeEnabled(long, int)}
+         */
+        public boolean isChangeEnabled(final long changeId, final int uid) {
+            return CompatChanges.isChangeEnabled(changeId, uid);
+        }
+
+        /**
+         * Retrieves native network total statistics.
+         *
+         * @return A NetworkStats.Entry containing the native statistics, or
+         *         null if an error occurs.
+         */
+        @Nullable
+        public NetworkStats.Entry nativeGetTotalStat() {
+            return NetworkStatsService.nativeGetTotalStat();
+        }
+
+        /**
+         * Retrieves native network interface statistics for the specified interface.
+         *
+         * @param iface The name of the network interface to query.
+         * @return A NetworkStats.Entry containing the native statistics for the interface, or
+         *         null if an error occurs.
+         */
+        @Nullable
+        public NetworkStats.Entry nativeGetIfaceStat(String iface) {
+            return NetworkStatsService.nativeGetIfaceStat(iface);
+        }
+
+        /**
+         * Retrieves native network uid statistics for the specified uid.
+         *
+         * @param uid The uid of the application to query.
+         * @return A NetworkStats.Entry containing the native statistics for the uid, or
+         *         null if an error occurs.
+         */
+        @Nullable
+        public NetworkStats.Entry nativeGetUidStat(int uid) {
+            return NetworkStatsService.nativeGetUidStat(uid);
+        }
     }
 
     /**
@@ -1783,6 +1945,8 @@
             if (transport == TRANSPORT_WIFI) {
                 ifaceSet = mAllWifiIfacesSinceBoot;
             } else if (transport == TRANSPORT_CELLULAR) {
+                // Since satellite networks appear under type mobile, this includes both cellular
+                // and satellite active interfaces
                 ifaceSet = mAllMobileIfacesSinceBoot;
             } else {
                 throw new IllegalArgumentException("Invalid transport " + transport);
@@ -1986,53 +2150,108 @@
         if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
             return UNSUPPORTED;
         }
-        return getEntryValueForType(nativeGetUidStat(uid), type);
+        if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, callingUid)) {
+            final NetworkStats.Entry entry = mTrafficStatsUidCache.getOrCompute(IFACE_ALL, uid,
+                    () -> mDeps.nativeGetUidStat(uid));
+            return getEntryValueForType(entry, type);
+        }
+
+        return getEntryValueForType(mDeps.nativeGetUidStat(uid), type);
+    }
+
+    @Nullable
+    private NetworkStats.Entry getIfaceStatsInternal(@NonNull String iface) {
+        final NetworkStats.Entry entry = mDeps.nativeGetIfaceStat(iface);
+        if (entry == null) {
+            return null;
+        }
+        // When tethering offload is in use, nativeIfaceStats does not contain usage from
+        // offload, add it back here. Note that the included statistics might be stale
+        // since polling newest stats from hardware might impact system health and not
+        // suitable for TrafficStats API use cases.
+        entry.add(getProviderIfaceStats(iface));
+        return entry;
     }
 
     @Override
     public long getIfaceStats(@NonNull String iface, int type) {
         Objects.requireNonNull(iface);
-        final NetworkStats.Entry entry = nativeGetIfaceStat(iface);
-        final long value = getEntryValueForType(entry, type);
-        if (value == UNSUPPORTED) {
-            return UNSUPPORTED;
-        } else {
-            // When tethering offload is in use, nativeIfaceStats does not contain usage from
-            // offload, add it back here. Note that the included statistics might be stale
-            // since polling newest stats from hardware might impact system health and not
-            // suitable for TrafficStats API use cases.
-            entry.add(getProviderIfaceStats(iface));
+        if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(
+                        ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
+            final NetworkStats.Entry entry = mTrafficStatsIfaceCache.getOrCompute(iface, UID_ALL,
+                    () -> getIfaceStatsInternal(iface));
             return getEntryValueForType(entry, type);
         }
+
+        return getEntryValueForType(getIfaceStatsInternal(iface), type);
     }
 
     private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
         if (entry == null) return UNSUPPORTED;
+        if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
         switch (type) {
-            case TrafficStats.TYPE_RX_BYTES:
+            case TYPE_RX_BYTES:
                 return entry.rxBytes;
-            case TrafficStats.TYPE_TX_BYTES:
-                return entry.txBytes;
-            case TrafficStats.TYPE_RX_PACKETS:
+            case TYPE_RX_PACKETS:
                 return entry.rxPackets;
-            case TrafficStats.TYPE_TX_PACKETS:
+            case TYPE_TX_BYTES:
+                return entry.txBytes;
+            case TYPE_TX_PACKETS:
                 return entry.txPackets;
             default:
-                return UNSUPPORTED;
+                throw new IllegalStateException("Bug: Invalid type: "
+                        + type + " should not reach here.");
         }
     }
 
+    private boolean isEntryValueTypeValid(int type) {
+        switch (type) {
+            case TYPE_RX_BYTES:
+            case TYPE_RX_PACKETS:
+            case TYPE_TX_BYTES:
+            case TYPE_TX_PACKETS:
+                return true;
+            default :
+                return false;
+        }
+    }
+
+    @Nullable
+    private NetworkStats.Entry getTotalStatsInternal() {
+        final NetworkStats.Entry entry = mDeps.nativeGetTotalStat();
+        if (entry == null) {
+            return null;
+        }
+        entry.add(getProviderIfaceStats(IFACE_ALL));
+        return entry;
+    }
+
     @Override
     public long getTotalStats(int type) {
-        final NetworkStats.Entry entry = nativeGetTotalStat();
-        final long value = getEntryValueForType(entry, type);
-        if (value == UNSUPPORTED) {
-            return UNSUPPORTED;
-        } else {
-            // Refer to comment in getIfaceStats
-            entry.add(getProviderIfaceStats(IFACE_ALL));
+        if (!isEntryValueTypeValid(type)) return UNSUPPORTED;
+        if (mAlwaysUseTrafficStatsRateLimitCache
+                || mDeps.isChangeEnabled(
+                        ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, Binder.getCallingUid())) {
+            final NetworkStats.Entry entry = mTrafficStatsTotalCache.getOrCompute(
+                    IFACE_ALL, UID_ALL, () -> getTotalStatsInternal());
             return getEntryValueForType(entry, type);
         }
+
+        return getEntryValueForType(getTotalStatsInternal(), type);
+    }
+
+    @Override
+    public void clearTrafficStatsRateLimitCaches() {
+        PermissionUtils.enforceNetworkStackPermissionOr(mContext, NETWORK_SETTINGS);
+        mTrafficStatsUidCache.clear();
+        mTrafficStatsIfaceCache.clear();
+        mTrafficStatsTotalCache.clear();
     }
 
     private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) {
@@ -2193,7 +2412,9 @@
         for (NetworkStateSnapshot snapshot : snapshots) {
             final int displayTransport =
                     getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes());
-            final boolean isMobile = (NetworkCapabilities.TRANSPORT_CELLULAR == displayTransport);
+            // Consider satellite transport to support satellite stats appear as type_mobile
+            final boolean isMobile = NetworkCapabilities.TRANSPORT_CELLULAR == displayTransport
+                    || NetworkCapabilities.TRANSPORT_SATELLITE == displayTransport;
             final boolean isWifi = (NetworkCapabilities.TRANSPORT_WIFI == displayTransport);
             final boolean isDefault = CollectionUtils.contains(
                     mDefaultNetworks, snapshot.getNetwork());
@@ -2326,12 +2547,14 @@
     }
 
     /**
-     * For networks with {@code TRANSPORT_CELLULAR}, get ratType that was obtained through
-     * {@link PhoneStateListener}. Otherwise, return 0 given that other networks with different
-     * transport types do not actually fill this value.
+     * For networks with {@code TRANSPORT_CELLULAR} Or {@code TRANSPORT_SATELLITE}, get ratType
+     * that was obtained through {@link PhoneStateListener}. Otherwise, return 0 given that other
+     * networks with different transport types do not actually fill this value.
      */
     private int getRatTypeForStateSnapshot(@NonNull NetworkStateSnapshot state) {
-        if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+        if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+                && !state.getNetworkCapabilities()
+                .hasTransport(NetworkCapabilities.TRANSPORT_SATELLITE)) {
             return 0;
         }
 
@@ -2470,8 +2693,22 @@
             performSampleLocked();
         }
 
-        // finally, dispatch updated event to any listeners
-        mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+        // Dispatch updated event to listeners, preventing intent spamming
+        // (b/343844995) possibly from abnormal modem RAT changes or misbehaving
+        // app calls (see NetworkStatsEventLogger#POLL_REASON_* for possible reasons).
+        // If no broadcasts are scheduled, use the time of the last broadcast
+        // to schedule the next one ASAP.
+        if (!mBroadcastNetworkStatsUpdatedRateLimitEnabled) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+        } else if (mLatestNetworkStatsUpdatedBroadcastScheduledTime < SystemClock.uptimeMillis()) {
+            mLatestNetworkStatsUpdatedBroadcastScheduledTime = Math.max(
+                    mLatestNetworkStatsUpdatedBroadcastScheduledTime
+                            + mSettings.getBroadcastNetworkStatsUpdateDelayMs(),
+                    SystemClock.uptimeMillis()
+            );
+            mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED),
+                    mLatestNetworkStatsUpdatedBroadcastScheduledTime);
+        }
 
         Trace.traceEnd(TRACE_TAG_NETWORK);
     }
@@ -2784,6 +3021,13 @@
             } catch (IOException e) {
                 pw.println("(failed to dump FastDataInput counters)");
             }
+            pw.print("trafficstats.cache.alwaysuse", mAlwaysUseTrafficStatsRateLimitCache);
+            pw.println();
+            pw.print(TRAFFIC_STATS_CACHE_EXPIRY_DURATION_NAME,
+                    mTrafficStatsRateLimitCacheExpiryDuration);
+            pw.println();
+            pw.print(TRAFFIC_STATS_CACHE_MAX_ENTRIES_NAME, mTrafficStatsRateLimitCacheMaxEntries);
+            pw.println();
 
             pw.decreaseIndent();
 
@@ -3423,6 +3667,11 @@
         public long getUidTagPersistBytes(long def) {
             return def;
         }
+
+        @Override
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
+        }
     }
 
     // TODO: Read stats by using BpfNetMapsReader.
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index 8598ac4..ca97d07 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -19,12 +19,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.NetworkStats;
+import android.util.LruCache;
 
 import com.android.internal.annotations.GuardedBy;
 
 import java.time.Clock;
-import java.util.HashMap;
 import java.util.Objects;
+import java.util.function.Supplier;
 
 /**
  * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
@@ -39,10 +40,12 @@
      *
      * @param clock The {@link Clock} to use for determining timestamps.
      * @param expiryDurationMs The expiry duration in milliseconds.
+     * @param maxSize Maximum number of entries.
      */
-    TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs) {
+    TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
         mClock = clock;
         mExpiryDurationMs = expiryDurationMs;
+        mMap = new LruCache<>(maxSize);
     }
 
     private static class TrafficStatsCacheKey {
@@ -81,7 +84,7 @@
     }
 
     @GuardedBy("mMap")
-    private final HashMap<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap = new HashMap<>();
+    private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
 
     /**
      * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
@@ -105,6 +108,36 @@
     }
 
     /**
+     * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
+     * If the entry is not found in the cache or has expired, computes it using the provided
+     * {@code supplier} and stores the result in the cache.
+     *
+     * @param iface The interface name to include in the cache key. {@code IFACE_ALL}
+     *              if not applicable.
+     * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+     * @param supplier The {@link Supplier} to compute the {@link NetworkStats.Entry} if not found.
+     * @return The cached or computed {@link NetworkStats.Entry}, or null if not found, expired,
+     *         or if the {@code supplier} returns null.
+     */
+    @Nullable
+    NetworkStats.Entry getOrCompute(String iface, int uid,
+            @NonNull Supplier<NetworkStats.Entry> supplier) {
+        synchronized (mMap) {
+            final NetworkStats.Entry cachedValue = get(iface, uid);
+            if (cachedValue != null) {
+                return cachedValue;
+            }
+
+            // Entry not found or expired, compute it
+            final NetworkStats.Entry computedEntry = supplier.get();
+            if (computedEntry != null && !computedEntry.isEmpty()) {
+                put(iface, uid, computedEntry);
+            }
+            return computedEntry;
+        }
+    }
+
+    /**
      * Stores a {@link NetworkStats.Entry} in the cache, associated with the given key.
      *
      * @param iface The interface name to include in the cache key. Null if not applicable.
@@ -124,7 +157,7 @@
      */
     void clear() {
         synchronized (mMap) {
-            mMap.clear();
+            mMap.evictAll();
         }
     }
 
diff --git a/service/Android.bp b/service/Android.bp
index c35c4f8..c68f0b8 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -188,18 +188,14 @@
         "androidx.annotation_annotation",
         "connectivity-net-module-utils-bpf",
         "connectivity_native_aidl_interface-lateststable-java",
-        "dnsresolver_aidl_interface-V14-java",
+        "dnsresolver_aidl_interface-V15-java",
         "modules-utils-shell-command-handler",
-        "net-utils-device-common",
-        "net-utils-device-common-ip",
-        "net-utils-device-common-netlink",
-        "net-utils-services-common",
+        "net-utils-service-connectivity",
         "netd-client",
         "networkstack-client",
         "PlatformProperties",
         "service-connectivity-protos",
         "service-connectivity-stats-protos",
-        "net-utils-multicast-forwarding-structs",
     ],
     apex_available: [
         "com.android.tethering",
@@ -210,6 +206,7 @@
     },
     visibility: [
         "//packages/modules/Connectivity/service-t",
+        "//packages/modules/Connectivity/networksecurity:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/thread/service:__subpackages__",
         "//packages/modules/Connectivity/thread/tests:__subpackages__",
@@ -251,6 +248,7 @@
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
         "service-nearby-pre-jarjar",
+        "service-networksecurity-pre-jarjar",
         service_remoteauth_pre_jarjar_lib,
         "service-thread-pre-jarjar",
     ],
@@ -319,6 +317,7 @@
         ":framework-connectivity-jarjar-rules",
         ":service-connectivity-jarjar-gen",
         ":service-nearby-jarjar-gen",
+        ":service-networksecurity-jarjar-gen",
         ":service-remoteauth-jarjar-gen",
         ":service-thread-jarjar-gen",
     ],
@@ -408,6 +407,24 @@
     visibility: ["//visibility:private"],
 }
 
+java_genrule {
+    name: "service-networksecurity-jarjar-gen",
+    tool_files: [
+        ":service-networksecurity-pre-jarjar{.jar}",
+        "jarjar-excludes.txt",
+    ],
+    tools: [
+        "jarjar-rules-generator",
+    ],
+    out: ["service_ct_jarjar_rules.txt"],
+    cmd: "$(location jarjar-rules-generator) " +
+        "$(location :service-networksecurity-pre-jarjar{.jar}) " +
+        "--prefix com.android.server.net.ct " +
+        "--excludes $(location jarjar-excludes.txt) " +
+        "--output $(out)",
+    visibility: ["//visibility:private"],
+}
+
 genrule {
     name: "statslog-connectivity-java-gen",
     tools: ["stats-log-api-gen"],
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index f7e47f5..4027038 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -31,4 +31,44 @@
     Thread Network regulatory purposes.
     -->
     <bool name="config_thread_location_use_for_country_code_enabled">true</bool>
+
+    <!-- Specifies the UTF-8 vendor name of this device. If this value is not an empty string, it
+    will be included in TXT value (key is 'vn') of the "_meshcop._udp" mDNS service which is
+    published by the Thread service. A non-empty string value must not exceed length of 24 UTF-8
+    bytes.
+    -->
+    <string translatable="false" name="config_thread_vendor_name">Android</string>
+
+    <!-- Specifies the 24 bits vendor OUI of this device. If this value is not an empty string, it
+    will be included in TXT (key is 'vo') value of the "_meshcop._udp" mDNS service which is
+    published by the Thread service. The OUI can be represented as a base-16 number of six
+    hexadecimal digits, or octets separated by hyphens or dots. For example, "ACDE48", "AC-DE-48"
+    and "AC:DE:48" are all valid representations of the same OUI value.
+    -->
+    <string translatable="false" name="config_thread_vendor_oui"></string>
+
+    <!-- Specifies the UTF-8 product model name of this device. If this value is not an empty
+    string, it will be included in TXT (key is 'mn') value of the "_meshcop._udp" mDNS service
+    which is published by the Thread service. A non-empty string value must not exceed length of 24
+    UTF-8 bytes.
+    -->
+    <string translatable="false" name="config_thread_model_name">Thread Border Router</string>
+
+    <!-- Specifies vendor-specific mDNS TXT entries which will be included in the "_meshcop._udp"
+    service. The TXT entries list MUST conform to the format requirement in RFC 6763 section 6. For
+    example, the key and value of each TXT entry MUST be separated with "=". If the value length is
+    0, the trailing "=" may be omitted. Additionally, the TXT keys MUST start with "v" and be at
+    least 2 characters.
+
+    Note, do not include credentials in any of the TXT entries - they will be advertised on Wi-Fi
+    or Ethernet link.
+
+    An example config can be:
+      <string-array name="config_thread_mdns_vendor_specific_txts">
+        <item>vab=123</item>
+        <item>vcd</item>
+      </string-array>
+    -->
+    <string-array name="config_thread_mdns_vendor_specific_txts">
+    </string-array>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index d9af5a3..fbaae05 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -48,6 +48,10 @@
             <!-- Configuration values for ThreadNetworkService -->
             <item type="bool" name="config_thread_default_enabled" />
             <item type="bool" name="config_thread_location_use_for_country_code_enabled" />
+            <item type="string" name="config_thread_vendor_name" />
+            <item type="string" name="config_thread_vendor_oui" />
+            <item type="string" name="config_thread_model_name" />
+            <item type="array" name="config_thread_mdns_vendor_specific_txts" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index c125bd6..c0082bb 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -80,7 +80,8 @@
       case VERIFY_BIN: return;
       case VERIFY_PROG:   fd = bpf::retrieveProgram(path); break;
       case VERIFY_MAP_RO: fd = bpf::mapRetrieveRO(path); break;
-      case VERIFY_MAP_RW: fd = bpf::mapRetrieveRW(path); break;
+      // lockless: we're just checking access rights and will immediately close the fd
+      case VERIFY_MAP_RW: fd = bpf::mapRetrieveLocklessRW(path); break;
     }
 
     if (fd < 0) ALOGF("bpf_obj_get '%s' failed, errno=%d", path, errno);
@@ -113,7 +114,8 @@
     if (!modules::sdklevel::IsAtLeastT()) return;
 
     V("/sys/fs/bpf", S_IFDIR|S_ISVTX|0777, ROOT, ROOT, "fs_bpf", DIR);
-    V("/sys/fs/bpf/net_shared", S_IFDIR|S_ISVTX|0777, ROOT, ROOT, "fs_bpf_net_shared", DIR);
+
+    V("/sys/fs/bpf/net_shared", S_IFDIR|01777, ROOT, ROOT, "fs_bpf_net_shared", DIR);
 
     // pre-U we do not have selinux privs to getattr on bpf maps/progs
     // so while the below *should* be as listed, we have no way to actually verify
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
index 3e11d52..b09589c 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -3,17 +3,6 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 33 (current min is 30): `getUidRule`"
-        errorLine1="        return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);"
-        errorLine2="                                ~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java"
-            line="643"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Call requires API level 31 (current min is 30): `BpfBitmap`"
         errorLine1="                return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);"
         errorLine2="                       ~~~~~~~~~~~~~">
diff --git a/service/src/com/android/server/BpfLoaderRcUtils.java b/service/src/com/android/server/BpfLoaderRcUtils.java
deleted file mode 100644
index 293e757..0000000
--- a/service/src/com/android/server/BpfLoaderRcUtils.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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.server;
-
-import android.annotation.NonNull;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * BpfRcUtils is responsible for comparing the bpf loader rc file.
- *
- * {@hide}
- */
-public class BpfLoaderRcUtils {
-    public static final String TAG = BpfLoaderRcUtils.class.getSimpleName();
-
-    private static final List<String> BPF_LOADER_RC_S_T = List.of(
-            "service bpfloader /system/bin/bpfloader",
-            "capabilities CHOWN SYS_ADMIN NET_ADMIN",
-            "rlimit memlock 1073741824 1073741824",
-            "oneshot",
-            "reboot_on_failure reboot,bpfloader-failed",
-            "updatable"
-    );
-
-    private static final List<String> BPF_LOADER_RC_U = List.of(
-            "service bpfloader /system/bin/bpfloader",
-            "capabilities CHOWN SYS_ADMIN NET_ADMIN",
-            "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
-            "user root",
-            "rlimit memlock 1073741824 1073741824",
-            "oneshot",
-            "reboot_on_failure reboot,bpfloader-failed",
-            "updatable"
-    );
-
-    private static final List<String> BPF_LOADER_RC_UQPR2 = List.of(
-            "service bpfloader /system/bin/netbpfload",
-            "capabilities CHOWN SYS_ADMIN NET_ADMIN",
-            "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
-            "user root",
-            "rlimit memlock 1073741824 1073741824",
-            "oneshot",
-            "reboot_on_failure reboot,bpfloader-failed",
-            "updatable"
-    );
-
-
-    private static final String BPF_LOADER_RC_FILE_PATH = "/etc/init/bpfloader.rc";
-    private static final String NET_BPF_LOAD_RC_FILE_PATH = "/etc/init/netbpfload.rc";
-
-    private BpfLoaderRcUtils() {
-    }
-
-    /**
-     * Load the bpf rc file content from the input stream.
-     */
-    @VisibleForTesting
-    public static List<String> loadExistingBpfRcFile(@NonNull InputStream inputStream) {
-        List<String> contents = new ArrayList<>();
-        boolean bpfSectionFound = false;
-        try (BufferedReader br = new BufferedReader(
-                new InputStreamReader(inputStream, StandardCharsets.ISO_8859_1))) {
-            String line;
-            while ((line = br.readLine()) != null) {
-                line = line.trim();
-                if (line.isEmpty()) {
-                    continue;
-                }
-                if (line.startsWith("#")) {
-                    continue;
-                }
-                // If bpf service section was found and new service or action section start. The
-                // read should stop.
-                if (bpfSectionFound && (line.startsWith("service ") || (line.startsWith("on ")))) {
-                    break;
-                }
-                if (line.startsWith("service bpfloader ")) {
-                    bpfSectionFound = true;
-                }
-                if (bpfSectionFound) {
-                    contents.add(line);
-                }
-            }
-        } catch (IOException e) {
-            Log.wtf("read input stream failed.", e);
-            contents.clear();
-            return contents;
-        }
-        return contents;
-    }
-
-    /**
-     * Check the bpfLoader rc file on the system image matches any of the template files.
-     */
-    public static boolean checkBpfLoaderRc() {
-        File bpfRcFile = new File(BPF_LOADER_RC_FILE_PATH);
-        if (!bpfRcFile.exists()) {
-            if (SdkLevel.isAtLeastU()) {
-                bpfRcFile = new File(NET_BPF_LOAD_RC_FILE_PATH);
-            }
-            if (!bpfRcFile.exists()) {
-                Log.wtf(TAG,
-                        "neither " + BPF_LOADER_RC_FILE_PATH + " nor " + NET_BPF_LOAD_RC_FILE_PATH
-                                + " exist.");
-                return false;
-            }
-            // Check bpf rc file in U QPR2
-            return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_UQPR2);
-        }
-
-        if (SdkLevel.isAtLeastU()) {
-            // Check bpf rc file in U
-            return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_U);
-        }
-        // Check bpf rc file in S/T
-        return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_S_T);
-    }
-
-    private static boolean compareBpfLoaderRc(@NonNull File bpfRcFile,
-            @NonNull List<String> template) {
-        try {
-            List<String> actualContent = loadExistingBpfRcFile(new FileInputStream(bpfRcFile));
-            if (!actualContent.equals(template)) {
-                Log.wtf(TAG, "BPF rc file is not same as the template files " + actualContent);
-                return false;
-            }
-        } catch (FileNotFoundException e) {
-            Log.wtf(bpfRcFile.getPath() + " doesn't exist.", e);
-            return false;
-        }
-        return true;
-    }
-}
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index a7fddd0..44868b2d 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -23,17 +23,17 @@
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
-import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.IIF_MATCH;
 import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
 import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
-import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
 import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
 import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
 import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
 import static android.net.BpfNetMapsUtils.isFirewallAllowList;
 import static android.net.BpfNetMapsUtils.matchToString;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.INetd.PERMISSION_INTERNET;
@@ -49,12 +49,13 @@
 
 import android.app.StatsManager;
 import android.content.Context;
-import android.net.BpfNetMapsReader;
+import android.net.BpfNetMapsUtils;
 import android.net.INetd;
 import android.net.UidOwnerValue;
 import android.os.Build;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.ArraySet;
@@ -71,6 +72,7 @@
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.SingleWriterBpfMap;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.Struct.S32;
 import com.android.net.module.util.Struct.U32;
@@ -187,7 +189,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U32> getConfigurationMap() {
         try {
-            return new BpfMap<>(
+            return SingleWriterBpfMap.getSingleton(
                     CONFIGURATION_MAP_PATH, S32.class, U32.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open netd configuration map", e);
@@ -197,7 +199,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
         try {
-            return new BpfMap<>(
+            return SingleWriterBpfMap.getSingleton(
                     UID_OWNER_MAP_PATH, S32.class, UidOwnerValue.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open uid owner map", e);
@@ -207,7 +209,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U8> getUidPermissionMap() {
         try {
-            return new BpfMap<>(
+            return SingleWriterBpfMap.getSingleton(
                     UID_PERMISSION_MAP_PATH, S32.class, U8.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open uid permission map", e);
@@ -217,6 +219,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
         try {
+            // Cannot use SingleWriterBpfMap because it's written by ClatCoordinator as well.
             return new BpfMap<>(COOKIE_TAG_MAP_PATH,
                     CookieTagMapKey.class, CookieTagMapValue.class);
         } catch (ErrnoException e) {
@@ -227,7 +230,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<S32, U8> getDataSaverEnabledMap() {
         try {
-            return new BpfMap<>(
+            return SingleWriterBpfMap.getSingleton(
                     DATA_SAVER_ENABLED_MAP_PATH, S32.class, U8.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open data saver enabled map", e);
@@ -237,7 +240,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private static IBpfMap<IngressDiscardKey, IngressDiscardValue> getIngressDiscardMap() {
         try {
-            return new BpfMap<>(INGRESS_DISCARD_MAP_PATH,
+            return SingleWriterBpfMap.getSingleton(INGRESS_DISCARD_MAP_PATH,
                     IngressDiscardKey.class, IngressDiscardValue.class);
         } catch (ErrnoException e) {
             throw new IllegalStateException("Cannot open ingress discard map", e);
@@ -446,62 +449,6 @@
     }
 
     /**
-     * Add naughty app bandwidth rule for specific app
-     *
-     * @param uid uid of target app
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void addNaughtyApp(final int uid) {
-        throwIfPreT("addNaughtyApp is not available on pre-T devices");
-
-        addRule(uid, PENALTY_BOX_MATCH, "addNaughtyApp");
-    }
-
-    /**
-     * Remove naughty app bandwidth rule for specific app
-     *
-     * @param uid uid of target app
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void removeNaughtyApp(final int uid) {
-        throwIfPreT("removeNaughtyApp is not available on pre-T devices");
-
-        removeRule(uid, PENALTY_BOX_MATCH, "removeNaughtyApp");
-    }
-
-    /**
-     * Add nice app bandwidth rule for specific app
-     *
-     * @param uid uid of target app
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void addNiceApp(final int uid) {
-        throwIfPreT("addNiceApp is not available on pre-T devices");
-
-        addRule(uid, HAPPY_BOX_MATCH, "addNiceApp");
-    }
-
-    /**
-     * Remove nice app bandwidth rule for specific app
-     *
-     * @param uid uid of target app
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void removeNiceApp(final int uid) {
-        throwIfPreT("removeNiceApp is not available on pre-T devices");
-
-        removeRule(uid, HAPPY_BOX_MATCH, "removeNiceApp");
-    }
-
-    /**
      * Set target firewall child chain
      *
      * @param childChain target chain to enable
@@ -535,14 +482,11 @@
      * @throws UnsupportedOperationException if called on pre-T devices.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
-     *
-     * @deprecated Use {@link BpfNetMapsReader#isChainEnabled} instead.
      */
-    // TODO: Migrate the callers to use {@link BpfNetMapsReader#isChainEnabled} instead.
     @Deprecated
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public boolean isChainEnabled(final int childChain) {
-        return BpfNetMapsReader.isChainEnabled(sConfigurationMap, childChain);
+        return BpfNetMapsUtils.isChainEnabled(sConfigurationMap, childChain);
     }
 
     private Set<Integer> asSet(final int[] uids) {
@@ -635,14 +579,13 @@
      * @throws UnsupportedOperationException if called on pre-T devices.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
-     *
-     * @deprecated use {@link BpfNetMapsReader#getUidRule} instead.
      */
-    // TODO: Migrate the callers to use {@link BpfNetMapsReader#getUidRule} instead.
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public int getUidRule(final int childChain, final int uid) {
-        return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);
+        return BpfNetMapsUtils.getUidRule(sUidOwnerMap, childChain, uid);
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private Set<Integer> getUidsMatchEnabled(final int childChain) throws ErrnoException {
         final long match = getMatchByFirewallChain(childChain);
         Set<Integer> uids = new ArraySet<>();
@@ -671,6 +614,7 @@
      * @param childChain target chain
      * @return Set of uids
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public Set<Integer> getUidsWithAllowRuleOnAllowListChain(final int childChain)
             throws ErrnoException {
         if (!isFirewallAllowList(childChain)) {
@@ -692,6 +636,7 @@
      * @param childChain target chain
      * @return Set of uids
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public Set<Integer> getUidsWithDenyRuleOnDenyListChain(final int childChain)
             throws ErrnoException {
         if (isFirewallAllowList(childChain)) {
@@ -865,6 +810,28 @@
     }
 
     /**
+     * Get granted permissions for specified uid. If uid is not in the map, this method returns
+     * {@link android.net.INetd.PERMISSION_INTERNET} since this is a default permission.
+     * See {@link #setNetPermForUids}
+     *
+     * @param uid target uid
+     * @return    granted permissions.
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public int getNetPermForUid(final int uid) {
+        final int appId = UserHandle.getAppId(uid);
+        try {
+            // Key of uid permission map is appId
+            // TODO: Rename map name
+            final U8 permissions = sUidPermissionMap.getValue(new S32(appId));
+            return permissions != null ? permissions.val : PERMISSION_INTERNET;
+        } catch (ErrnoException e) {
+            Log.wtf(TAG, "Failed to get permission for uid: " + uid);
+            return PERMISSION_INTERNET;
+        }
+    }
+
+    /**
      * Set Data Saver enabled or disabled
      *
      * @param enable     whether Data Saver is enabled or disabled.
@@ -924,6 +891,49 @@
         }
     }
 
+    /**
+     * Get blocked reasons for specified uid
+     *
+     * @param uid Target Uid
+     * @return Reasons of network access blocking for an UID
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public int getUidNetworkingBlockedReasons(final int uid) {
+        return BpfNetMapsUtils.getUidNetworkingBlockedReasons(uid,
+                sConfigurationMap, sUidOwnerMap, sDataSaverEnabledMap);
+    }
+
+    /**
+     * Return whether the network access of specified uid is blocked on metered networks
+     *
+     * @param uid The target uid.
+     * @return True if the network access is blocked on metered networks. Otherwise, false
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public boolean isUidRestrictedOnMeteredNetworks(final int uid) {
+        final int blockedReasons = getUidNetworkingBlockedReasons(uid);
+        return (blockedReasons & BLOCKED_METERED_REASON_MASK) != BLOCKED_REASON_NONE;
+    }
+
+    /*
+     * Return whether the network is blocked by firewall chains for the given uid.
+     *
+     * Note that {@link #getDataSaverEnabled()} has a latency before V.
+     *
+     * @param uid The target uid.
+     * @param isNetworkMetered Whether the target network is metered.
+     *
+     * @return True if the network is blocked. Otherwise, false.
+     * @throws ServiceSpecificException if the read fails.
+     *
+     * @hide
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered) {
+        return BpfNetMapsUtils.isUidNetworkingBlocked(uid, isNetworkMetered,
+                sConfigurationMap, sUidOwnerMap, sDataSaverEnabledMap);
+    }
+
     /** Register callback for statsd to pull atom. */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public void setPullAtomCallback(final Context context) {
@@ -986,6 +996,7 @@
         return sj.toString();
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private void dumpOwnerMatchConfig(final IndentingPrintWriter pw) {
         try {
             final long match = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
diff --git a/service/src/com/android/server/CallbackQueue.java b/service/src/com/android/server/CallbackQueue.java
new file mode 100644
index 0000000..4e068ea
--- /dev/null
+++ b/service/src/com/android/server/CallbackQueue.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.annotation.NonNull;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+
+import com.android.net.module.util.GrowingIntArray;
+
+/**
+ * A utility class to add/remove {@link NetworkCallback}s from a queue.
+ *
+ * <p>This is intended to be used as a temporary builder to create/modify callbacks stored in an int
+ * array for memory efficiency.
+ *
+ * <p>Intended usage:
+ * <pre>
+ *     final CallbackQueue queue = new CallbackQueue(storedCallbacks);
+ *     queue.forEach(netId, callbackId -> { [...] });
+ *     queue.addCallback(netId, callbackId);
+ *     [...]
+ *     storedCallbacks = queue.getMinimizedBackingArray();
+ * </pre>
+ *
+ * <p>This class is not thread-safe.
+ */
+public class CallbackQueue extends GrowingIntArray {
+    public CallbackQueue(int[] initialCallbacks) {
+        super(initialCallbacks);
+    }
+
+    /**
+     * Get a callback int from netId and callbackId.
+     *
+     * <p>The first 16 bits of each int is the netId; the last 16 bits are the callback index.
+     */
+    private static int getCallbackInt(int netId, int callbackId) {
+        return (netId << 16) | (callbackId & 0xffff);
+    }
+
+    private static int getNetId(int callbackInt) {
+        return callbackInt >>> 16;
+    }
+
+    private static int getCallbackId(int callbackInt) {
+        return callbackInt & 0xffff;
+    }
+
+    /**
+     * A consumer interface for {@link #forEach(CallbackConsumer)}.
+     *
+     * <p>This is similar to a BiConsumer&lt;Integer, Integer&gt;, but avoids the boxing cost.
+     */
+    public interface CallbackConsumer {
+        /**
+         * Method called on each callback in the queue.
+         */
+        void accept(int netId, int callbackId);
+    }
+
+    /**
+     * Iterate over all callbacks in the queue.
+     */
+    public void forEach(@NonNull CallbackConsumer consumer) {
+        forEach(value -> {
+            final int netId = getNetId(value);
+            final int callbackId = getCallbackId(value);
+            consumer.accept(netId, callbackId);
+        });
+    }
+
+    /**
+     * Indicates whether the queue contains a callback for the given (netId, callbackId).
+     */
+    public boolean hasCallback(int netId, int callbackId) {
+        return contains(getCallbackInt(netId, callbackId));
+    }
+
+    /**
+     * Remove all callbacks for the given netId.
+     *
+     * @return true if at least one callback was removed.
+     */
+    public boolean removeCallbacksForNetId(int netId) {
+        return removeValues(cb -> getNetId(cb) == netId);
+    }
+
+    /**
+     * Remove all callbacks for the given netId and callbackId.
+     * @return true if at least one callback was removed.
+     */
+    public boolean removeCallbacks(int netId, int callbackId) {
+        final int cbInt = getCallbackInt(netId, callbackId);
+        return removeValues(cb -> cb == cbInt);
+    }
+
+    /**
+     * Add a callback at the end of the queue.
+     */
+    public void addCallback(int netId, int callbackId) {
+        add(getCallbackInt(netId, callbackId));
+    }
+
+    @Override
+    protected String valueToString(int item) {
+        final int callbackId = getCallbackId(item);
+        final int netId = getNetId(item);
+        return ConnectivityManager.getCallbackName(callbackId) + "(" + netId + ")";
+    }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 6839c22..0fe24a2 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -24,6 +24,8 @@
 import static android.content.pm.PackageManager.FEATURE_WIFI;
 import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.BpfNetMapsConstants.METERED_ALLOW_CHAINS;
+import static android.net.BpfNetMapsConstants.METERED_DENY_CHAINS;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
@@ -34,14 +36,29 @@
 import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
 import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.CALLBACK_AVAILABLE;
+import static android.net.ConnectivityManager.CALLBACK_BLK_CHANGED;
+import static android.net.ConnectivityManager.CALLBACK_CAP_CHANGED;
 import static android.net.ConnectivityManager.CALLBACK_IP_CHANGED;
+import static android.net.ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED;
+import static android.net.ConnectivityManager.CALLBACK_LOSING;
+import static android.net.ConnectivityManager.CALLBACK_LOST;
+import static android.net.ConnectivityManager.CALLBACK_PRECHECK;
+import static android.net.ConnectivityManager.CALLBACK_RESUMED;
+import static android.net.ConnectivityManager.CALLBACK_SUSPENDED;
+import static android.net.ConnectivityManager.CALLBACK_UNAVAIL;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
+import static android.net.ConnectivityManager.NETID_UNSET;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_NONE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
@@ -62,6 +79,7 @@
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED;
@@ -72,6 +90,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -97,29 +116,45 @@
 import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION;
+import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.VPN_UID;
 import static android.system.OsConstants.ETH_P_ALL;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_GETSOCKOPT;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_CONNECT;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_CONNECT;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_EGRESS;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_INGRESS;
 import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_SOCK_CREATE;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_SOCK_RELEASE;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_SETSOCKOPT;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP4_RECVMSG;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP4_SENDMSG;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_RECVMSG;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_UDP6_SENDMSG;
 import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
 import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
 import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
 import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
+import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
+import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
+import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
 import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 
 import android.Manifest;
 import android.annotation.CheckResult;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresApi;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
@@ -214,7 +249,6 @@
 import android.net.Uri;
 import android.net.VpnManager;
 import android.net.VpnTransportInfo;
-import android.net.connectivity.ConnectivityCompatChanges;
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.NetworkEvent;
 import android.net.netd.aidl.NativeUidRangeConfig;
@@ -269,8 +303,6 @@
 import android.util.SparseIntArray;
 import android.util.StatsEvent;
 
-import androidx.annotation.RequiresApi;
-
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -301,6 +333,7 @@
 import com.android.net.module.util.LocationPermissionChecker;
 import com.android.net.module.util.PerUidCounter;
 import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.TcUtils;
 import com.android.net.module.util.netlink.InetDiagMessage;
 import com.android.networkstack.apishim.BroadcastOptionsShimImpl;
@@ -338,7 +371,6 @@
 import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
 import com.android.server.connectivity.ProxyTracker;
 import com.android.server.connectivity.QosCallbackTracker;
-import com.android.server.connectivity.RoutingCoordinatorService;
 import com.android.server.connectivity.SatelliteAccessController;
 import com.android.server.connectivity.UidRangeUtils;
 import com.android.server.connectivity.VpnNetworkPreferenceInfo;
@@ -392,6 +424,8 @@
     private static final String NETWORK_ARG = "networks";
     private static final String REQUEST_ARG = "requests";
     private static final String TRAFFICCONTROLLER_ARG = "trafficcontroller";
+    public static final String CLATEGRESS4RAWBPFMAP_ARG = "clatEgress4RawBpfMap";
+    public static final String CLATINGRESS6RAWBPFMAP_ARG = "clatIngress6RawBpfMap";
 
     private static final boolean DBG = true;
     private static final boolean DDBG = Log.isLoggable(TAG, Log.DEBUG);
@@ -474,10 +508,38 @@
     private volatile boolean mLockdownEnabled;
 
     private final boolean mRequestRestrictedWifiEnabled;
+    private final boolean mBackgroundFirewallChainEnabled;
+
+    private final boolean mUseDeclaredMethodsForCallbacksEnabled;
+
+    // Flag to delay callbacks for frozen apps, suppressing duplicate and stale callbacks.
+    private final boolean mQueueCallbacksForFrozenApps;
 
     /**
-     * Stale copy of uid blocked reasons provided by NPMS. As long as they are accessed only in
-     * internal handler thread, they don't need a lock.
+     * Uids ConnectivityService tracks blocked status of to send blocked status callbacks.
+     * Key is uid based on mAsUid of registered networkRequestInfo
+     * Value is count of registered networkRequestInfo
+     *
+     * This is necessary because when a firewall chain is enabled or disabled, that affects all UIDs
+     * on the system, not just UIDs on that firewall chain. For example, entering doze mode affects
+     * all UIDs that are not on the dozable chain. ConnectivityService doesn't know which UIDs are
+     * running. But it only needs to send onBlockedStatusChanged to UIDs that have at least one
+     * NetworkCallback registered.
+     *
+     * UIDs are added to this list on the binder thread when processing requestNetwork and similar
+     * IPCs. They are removed from this list on the handler thread, when the callback unregistration
+     * is fully processed. They cannot be unregistered when the unregister IPC is processed because
+     * sometimes requests are unregistered on the handler thread.
+     */
+    @GuardedBy("mBlockedStatusTrackingUids")
+    private final SparseIntArray mBlockedStatusTrackingUids = new SparseIntArray();
+
+    /**
+     * Stale copy of UID blocked reasons. This is used to send onBlockedStatusChanged
+     * callbacks. This is only used on the handler thread, so it does not require a lock.
+     * On U-, the blocked reasons come from NPMS.
+     * On V+, the blocked reasons come from the BPF map contents and only maintains blocked reasons
+     * of uids that register network callbacks.
      */
     private final SparseIntArray mUidBlockedReasons = new SparseIntArray();
 
@@ -545,6 +607,8 @@
      * default network for each app.
      * Order ints passed to netd must be in the 0~999 range. Larger values code for
      * a lower priority, see {@link NativeUidRangeConfig}.
+     * Note that only the highest priority preference is applied if the uid is the target of
+     * multiple preferences.
      *
      * Requests that don't code for a per-app preference use PREFERENCE_ORDER_INVALID.
      * The default request uses PREFERENCE_ORDER_DEFAULT.
@@ -779,11 +843,10 @@
     private static final int EVENT_SET_PROFILE_NETWORK_PREFERENCE = 50;
 
     /**
-     * Event to specify that reasons for why an uid is blocked changed.
-     * arg1 = uid
-     * arg2 = blockedReasons
+     * Event to update blocked reasons for uids.
+     * obj = List of Pair(uid, blockedReasons)
      */
-    private static final int EVENT_UID_BLOCKED_REASON_CHANGED = 51;
+    private static final int EVENT_BLOCKED_REASONS_CHANGED = 51;
 
     /**
      * Event to register a new network offer
@@ -844,9 +907,16 @@
     private static final int EVENT_UID_FROZEN_STATE_CHANGED = 61;
 
     /**
-     * Event to inform the ConnectivityService handler when a uid has lost carrier privileges.
+     * Event to update firewall socket destroy reasons for uids.
+     * obj = List of Pair(uid, socketDestroyReasons)
      */
-    private static final int EVENT_UID_CARRIER_PRIVILEGES_LOST = 62;
+    private static final int EVENT_UPDATE_FIREWALL_DESTROY_SOCKET_REASONS = 62;
+
+    /**
+     * Event to clear firewall socket destroy reasons for all uids.
+     * arg1 = socketDestroyReason
+     */
+    private static final int EVENT_CLEAR_FIREWALL_DESTROY_SOCKET_REASONS = 63;
 
     /**
      * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
@@ -992,14 +1062,22 @@
     // Flag to enable the feature of closing frozen app sockets.
     private final boolean mDestroyFrozenSockets;
 
-    // Flag to optimize closing frozen app sockets by waiting for the cellular modem to wake up.
-    private final boolean mDelayDestroyFrozenSockets;
+    // Flag to optimize closing app sockets by waiting for the cellular modem to wake up.
+    private final boolean mDelayDestroySockets;
 
     // Flag to allow SysUI to receive connectivity reports for wifi picker UI.
     private final boolean mAllowSysUiConnectivityReports;
 
     // Uids that ConnectivityService is pending to close sockets of.
-    private final Set<Integer> mPendingFrozenUids = new ArraySet<>();
+    // Key is uid and value is reasons of socket destroy
+    private final SparseIntArray mDestroySocketPendingUids = new SparseIntArray();
+
+    private static final int DESTROY_SOCKET_REASON_NONE = 0;
+    private static final int DESTROY_SOCKET_REASON_FROZEN = 1 << 0;
+    private static final int DESTROY_SOCKET_REASON_FIREWALL_BACKGROUND = 1 << 1;
+
+    // Flag to drop packets to VPN addresses ingressing via non-VPN interfaces.
+    private final boolean mIngressToVpnAddressFiltering;
 
     /**
      * Implements support for the legacy "one network per network type" model.
@@ -1288,14 +1366,6 @@
     }
     private final LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker(this);
 
-    @VisibleForTesting
-    void onCarrierPrivilegesLost(Integer uid, Integer subId) {
-        if (mRequestRestrictedWifiEnabled) {
-            mHandler.sendMessage(mHandler.obtainMessage(
-                    EVENT_UID_CARRIER_PRIVILEGES_LOST, uid, subId));
-        }
-    }
-
     final LocalPriorityDump mPriorityDumper = new LocalPriorityDump();
     /**
      * Helper class which parses out priority arguments and dumps sections according to their
@@ -1517,10 +1587,11 @@
                 @NonNull final Context context,
                 @NonNull final TelephonyManager tm,
                 boolean requestRestrictedWifiEnabled,
-                @NonNull BiConsumer<Integer, Integer> listener) {
+                @NonNull BiConsumer<Integer, Integer> listener,
+                @NonNull final Handler connectivityServiceHandler) {
             if (isAtLeastT()) {
-                return new CarrierPrivilegeAuthenticator(
-                        context, tm, requestRestrictedWifiEnabled, listener);
+                return new CarrierPrivilegeAuthenticator(context, tm, requestRestrictedWifiEnabled,
+                        listener, connectivityServiceHandler);
             } else {
                 return null;
             }
@@ -1732,7 +1803,7 @@
         mDefaultRequest = new NetworkRequestInfo(
                 Process.myUid(), defaultInternetRequest, null,
                 null /* binder */, NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
-                null /* attributionTags */);
+                null /* attributionTags */, DECLARED_METHODS_NONE);
         mNetworkRequests.put(defaultInternetRequest, mDefaultRequest);
         mDefaultNetworkRequests.add(mDefaultRequest);
         mNetworkRequestInfoLogs.log("REGISTER " + mDefaultRequest);
@@ -1803,9 +1874,16 @@
         mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
         mRequestRestrictedWifiEnabled = mDeps.isAtLeastU()
                 && mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
+        mBackgroundFirewallChainEnabled = mDeps.isAtLeastV() && mDeps.isFeatureNotChickenedOut(
+                context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
+        mUseDeclaredMethodsForCallbacksEnabled = mDeps.isFeatureEnabled(context,
+                ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
+        // registerUidFrozenStateChangedCallback is only available on U+
+        mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
+                && mDeps.isFeatureEnabled(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
         mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
                 mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
-                this::onCarrierPrivilegesLost);
+                this::handleUidCarrierPrivilegesLost, mHandler);
 
         if (mDeps.isAtLeastU()
                 && mDeps
@@ -1819,7 +1897,12 @@
         // To ensure uid state is synchronized with Network Policy, register for
         // NetworkPolicyManagerService events must happen prior to NetworkPolicyManagerService
         // reading existing policy from disk.
-        mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);
+        // If shouldTrackUidsForBlockedStatusCallbacks() is true (On V+), ConnectivityService
+        // updates blocked reasons when firewall chain and data saver status is updated based on
+        // bpf map contents instead of receiving callbacks from NPMS
+        if (!shouldTrackUidsForBlockedStatusCallbacks()) {
+            mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);
+        }
 
         final PowerManager powerManager = (PowerManager) context.getSystemService(
                 Context.POWER_SERVICE);
@@ -1950,11 +2033,10 @@
 
         mDestroyFrozenSockets = mDeps.isAtLeastV() || (mDeps.isAtLeastU()
                 && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION));
-        mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
-                && mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
+        mDelayDestroySockets = mDeps.isFeatureNotChickenedOut(context, DELAY_DESTROY_SOCKETS);
         mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
                 mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
-        if (mDestroyFrozenSockets) {
+        if (mDestroyFrozenSockets || mQueueCallbacksForFrozenApps) {
             final UidFrozenStateChangedCallback frozenStateChangedCallback =
                     new UidFrozenStateChangedCallback() {
                 @Override
@@ -1978,10 +2060,8 @@
             activityManager.registerUidFrozenStateChangedCallback(
                     (Runnable r) -> r.run(), frozenStateChangedCallback);
         }
-
-        if (mDeps.isFeatureNotChickenedOut(mContext, LOG_BPF_RC)) {
-            mHandler.post(BpfLoaderRcUtils::checkBpfLoaderRc);
-        }
+        mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
+                && mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
     }
 
     /**
@@ -2068,7 +2148,7 @@
     }
 
     @VisibleForTesting
-    void updateMobileDataPreferredUids() {
+    public void updateMobileDataPreferredUids() {
         mHandler.sendEmptyMessage(EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
     }
 
@@ -2113,7 +2193,7 @@
             handleRegisterNetworkRequest(new NetworkRequestInfo(
                     Process.myUid(), networkRequest, null /* messenger */, null /* binder */,
                     NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
-                    null /* attributionTags */));
+                    null /* attributionTags */, DECLARED_METHODS_NONE));
         } else {
             handleReleaseNetworkRequest(networkRequest, Process.SYSTEM_UID,
                     /* callOnUnavailable */ false);
@@ -2229,6 +2309,11 @@
         return nai;
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private boolean hasInternetPermission(final int uid) {
+        return (mBpfNetMaps.getNetPermForUid(uid) & PERMISSION_INTERNET) != 0;
+    }
+
     /**
      * Check if UID should be blocked from using the specified network.
      */
@@ -2242,7 +2327,17 @@
         final long ident = Binder.clearCallingIdentity();
         try {
             final boolean metered = nc == null ? true : nc.isMetered();
-            return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+            if (mDeps.isAtLeastV()) {
+                final boolean hasInternetPermission = hasInternetPermission(uid);
+                final boolean blockedByUidRules = mBpfNetMaps.isUidNetworkingBlocked(uid, metered);
+                if (mDeps.isChangeEnabled(NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, uid)) {
+                    return blockedByUidRules || !hasInternetPermission;
+                } else {
+                    return hasInternetPermission && blockedByUidRules;
+                }
+            } else {
+                return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+            }
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -2320,7 +2415,7 @@
         final int uid = mDeps.getCallingUid();
         final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
         if (nai == null) return null;
-        final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false);
+        final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false /* ignoreBlocked */);
         maybeLogBlockedNetworkInfo(networkInfo, uid);
         return networkInfo;
     }
@@ -2611,7 +2706,7 @@
                 // Not the system, so it's an app requesting on its own behalf.
                 type = RequestType.RT_APP.getNumber();
             }
-            countPerType.put(type, countPerType.get(type, 0));
+            countPerType.put(type, countPerType.get(type, 0) + 1);
         }
         for (int i = countPerType.size() - 1; i >= 0; --i) {
             final RequestCountForType.Builder r = RequestCountForType.newBuilder();
@@ -2769,6 +2864,7 @@
 
     private boolean canSeeAllowedUids(final int pid, final int uid, final int netOwnerUid) {
         return Process.SYSTEM_UID == uid
+                || netOwnerUid == uid
                 || hasAnyPermissionOf(mContext, pid, uid,
                         android.Manifest.permission.NETWORK_FACTORY);
     }
@@ -2796,7 +2892,6 @@
         }
         if (!canSeeAllowedUids(callerPid, callerUid, newNc.getOwnerUid())) {
             newNc.setAllowedUids(new ArraySet<>());
-            newNc.setSubscriptionIds(Collections.emptySet());
         }
         redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid);
 
@@ -3024,6 +3119,23 @@
         }
     }
 
+    private void maybeDisableLocalNetworkMatching(NetworkCapabilities nc, int callingUid) {
+        if (mDeps.isChangeEnabled(ENABLE_MATCH_LOCAL_NETWORK, callingUid)) {
+            return;
+        }
+        // If NET_CAPABILITY_LOCAL_NETWORK is not added to capability, request should not be
+        // satisfied by local networks.
+        if (!nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+            nc.addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        }
+    }
+
+    private void restrictRequestNetworkCapabilitiesForCaller(NetworkCapabilities nc,
+            int callingUid, String callerPackageName) {
+        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callerPackageName);
+        maybeDisableLocalNetworkMatching(nc, callingUid);
+    }
+
     @Override
     public @RestrictBackgroundStatus int getRestrictBackgroundStatusByCaller() {
         enforceAccessPermission();
@@ -3277,14 +3389,38 @@
     private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {
         @Override
         public void onUidBlockedReasonChanged(int uid, @BlockedReason int blockedReasons) {
-            mHandler.sendMessage(mHandler.obtainMessage(EVENT_UID_BLOCKED_REASON_CHANGED,
-                    uid, blockedReasons));
+            if (shouldTrackUidsForBlockedStatusCallbacks()) {
+                Log.wtf(TAG, "Received unexpected NetworkPolicy callback");
+                return;
+            }
+            mHandler.sendMessage(mHandler.obtainMessage(
+                    EVENT_BLOCKED_REASONS_CHANGED,
+                    List.of(new Pair<>(uid, blockedReasons))));
         }
     };
 
-    private void handleUidBlockedReasonChanged(int uid, @BlockedReason int blockedReasons) {
-        maybeNotifyNetworkBlockedForNewState(uid, blockedReasons);
-        setUidBlockedReasons(uid, blockedReasons);
+    private boolean shouldTrackUidsForBlockedStatusCallbacks() {
+        return mDeps.isAtLeastV();
+    }
+
+    @VisibleForTesting
+    public void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
+        for (Pair<Integer, Integer> reasons: reasonsList) {
+            final int uid = reasons.first;
+            final int blockedReasons = reasons.second;
+            if (shouldTrackUidsForBlockedStatusCallbacks()) {
+                synchronized (mBlockedStatusTrackingUids) {
+                    if (mBlockedStatusTrackingUids.get(uid) == 0) {
+                        // This uid is not tracked anymore.
+                        // This can happen if the network request is unregistered while
+                        // EVENT_BLOCKED_REASONS_CHANGED is posted but not processed yet.
+                        continue;
+                    }
+                }
+            }
+            maybeNotifyNetworkBlockedForNewState(uid, blockedReasons);
+            setUidBlockedReasons(uid, blockedReasons);
+        }
     }
 
     static final class UidFrozenStateChangedArgs {
@@ -3315,44 +3451,167 @@
         return !mNetworkActivityTracker.isDefaultNetworkActive();
     }
 
-    private void handleFrozenUids(int[] uids, int[] frozenStates) {
-        final ArraySet<Integer> ownerUids = new ArraySet<>();
+    private boolean shouldTrackFirewallDestroySocketReasons() {
+        return mDeps.isAtLeastV();
+    }
 
-        for (int i = 0; i < uids.length; i++) {
-            if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
-                ownerUids.add(uids[i]);
-            } else {
-                mPendingFrozenUids.remove(uids[i]);
-            }
-        }
-
-        if (ownerUids.isEmpty()) {
-            return;
-        }
-
-        if (mDelayDestroyFrozenSockets && isCellNetworkIdle()) {
-            // Delay closing sockets to avoid waking the cell modem up.
-            // Wi-Fi network state is not considered since waking Wi-Fi modem up is much cheaper
-            // than waking cell modem up.
-            mPendingFrozenUids.addAll(ownerUids);
+    private void updateDestroySocketReasons(final int uid, final int reason,
+            final boolean addReason) {
+        final int destroyReasons = mDestroySocketPendingUids.get(uid, DESTROY_SOCKET_REASON_NONE);
+        if (addReason) {
+            mDestroySocketPendingUids.put(uid, destroyReasons | reason);
         } else {
-            try {
-                mDeps.destroyLiveTcpSocketsByOwnerUids(ownerUids);
-            } catch (SocketException | InterruptedIOException | ErrnoException e) {
-                loge("Exception in socket destroy: " + e);
+            final int newDestroyReasons = destroyReasons & ~reason;
+            if (newDestroyReasons == DESTROY_SOCKET_REASON_NONE) {
+                mDestroySocketPendingUids.delete(uid);
+            } else {
+                mDestroySocketPendingUids.put(uid, newDestroyReasons);
             }
         }
     }
 
-    private void closePendingFrozenSockets() {
+    private void handleFrozenUids(int[] uids, int[] frozenStates) {
+        ensureRunningOnConnectivityServiceThread();
+        handleDestroyFrozenSockets(uids, frozenStates);
+        handleFreezeNetworkCallbacks(uids, frozenStates);
+    }
+
+    private void handleDestroyFrozenSockets(int[] uids, int[] frozenStates) {
+        if (!mDestroyFrozenSockets) {
+            return;
+        }
+        for (int i = 0; i < uids.length; i++) {
+            final int uid = uids[i];
+            final boolean addReason = frozenStates[i] == UID_FROZEN_STATE_FROZEN;
+            updateDestroySocketReasons(uid, DESTROY_SOCKET_REASON_FROZEN, addReason);
+        }
+
+        if (!mDelayDestroySockets || !isCellNetworkIdle()) {
+            destroyPendingSockets();
+        }
+    }
+
+    private void handleFreezeNetworkCallbacks(int[] uids, int[] frozenStates) {
+        if (!mQueueCallbacksForFrozenApps) {
+            return;
+        }
+        for (int i = 0; i < uids.length; i++) {
+            final int uid = uids[i];
+            // These counters may be modified on different threads, but using them here is fine
+            // because this is only an optimization where wrong behavior would only happen if they
+            // are zero even though there is a request registered. This is not possible as they are
+            // always incremented before posting messages to register, and decremented on the
+            // handler thread when unregistering.
+            if (mSystemNetworkRequestCounter.get(uid) == 0
+                    && mNetworkRequestCounter.get(uid) == 0) {
+                // Avoid iterating requests if there isn't any. The counters only track app requests
+                // and not internal requests (for example always-on requests which do not have a
+                // mMessenger), so it does not completely match the content of mRequests. This is OK
+                // as only app requests need to be frozen.
+                continue;
+            }
+
+            if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
+                freezeNetworkCallbacksForUid(uid);
+            } else {
+                unfreezeNetworkCallbacksForUid(uid);
+            }
+        }
+    }
+
+    /**
+     * Suspend callbacks for a UID that was just frozen.
+     *
+     * <p>Note that it is not possible for a process to be frozen during a blocking binder call
+     * (see CachedAppOptimizer.freezeBinder), and IConnectivityManager callback registrations are
+     * blocking binder calls, so no callback can be registered while the UID is frozen. This means
+     * it is not necessary to check frozen state on new callback registrations, and calling this
+     * method when a UID is newly frozen is sufficient.
+     *
+     * <p>If it ever becomes possible for a process to be frozen during a blocking binder call,
+     * ConnectivityService will need to handle freezing callbacks that reach ConnectivityService
+     * after the app was frozen when being registered.
+     */
+    private void freezeNetworkCallbacksForUid(int uid) {
+        if (DDBG) Log.d(TAG, "Freezing callbacks for UID " + uid);
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (nri.mUid != uid) continue;
+            // mNetworkRequests can have duplicate values for multilayer requests, but calling
+            // onFrozen multiple times is fine.
+            // If freezeNetworkCallbacksForUid was called multiple times in a raw for a frozen UID
+            // (which would be incorrect), this would also handle it gracefully.
+            nri.onFrozen();
+        }
+    }
+
+    private void unfreezeNetworkCallbacksForUid(int uid) {
+        // This sends all callbacks for one NetworkRequest at a time, which may not be the
+        // same order they were queued in, but different network requests use different
+        // binder objects, so the relative order of their callbacks is not guaranteed.
+        // If callbacks are not queued, callbacks from different binder objects may be
+        // posted on different threads when the process is unfrozen, so even if they were
+        // called a long time apart while the process was frozen, they may still appear in
+        // different order when unfreezing it.
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (nri.mUid != uid) continue;
+            nri.sendQueuedCallbacks();
+        }
+    }
+
+    private void handleUpdateFirewallDestroySocketReasons(
+            List<Pair<Integer, Integer>> reasonsList) {
+        if (!shouldTrackFirewallDestroySocketReasons()) {
+            Log.wtf(TAG, "handleUpdateFirewallDestroySocketReasons is called unexpectedly");
+            return;
+        }
         ensureRunningOnConnectivityServiceThread();
 
-        try {
-            mDeps.destroyLiveTcpSocketsByOwnerUids(mPendingFrozenUids);
-        } catch (SocketException | InterruptedIOException | ErrnoException e) {
-            loge("Failed to close pending frozen app sockets: " + e);
+        for (Pair<Integer, Integer> uidSocketDestroyReasons: reasonsList) {
+            final int uid = uidSocketDestroyReasons.first;
+            final int reasons = uidSocketDestroyReasons.second;
+            final boolean destroyByFirewallBackground =
+                    (reasons & DESTROY_SOCKET_REASON_FIREWALL_BACKGROUND)
+                            != DESTROY_SOCKET_REASON_NONE;
+            updateDestroySocketReasons(uid, DESTROY_SOCKET_REASON_FIREWALL_BACKGROUND,
+                    destroyByFirewallBackground);
         }
-        mPendingFrozenUids.clear();
+
+        if (!mDelayDestroySockets || !isCellNetworkIdle()) {
+            destroyPendingSockets();
+        }
+    }
+
+    private void handleClearFirewallDestroySocketReasons(final int reason) {
+        if (!shouldTrackFirewallDestroySocketReasons()) {
+            Log.wtf(TAG, "handleClearFirewallDestroySocketReasons is called uexpectedly");
+            return;
+        }
+        ensureRunningOnConnectivityServiceThread();
+
+        // Unset reason from all pending uids
+        for (int i = mDestroySocketPendingUids.size() - 1; i >= 0; i--) {
+            final int uid = mDestroySocketPendingUids.keyAt(i);
+            updateDestroySocketReasons(uid, reason, false /* addReason */);
+        }
+    }
+
+    private void destroyPendingSockets() {
+        ensureRunningOnConnectivityServiceThread();
+        if (mDestroySocketPendingUids.size() == 0) {
+            return;
+        }
+
+        Set<Integer> uids = new ArraySet<>();
+        for (int i = 0; i < mDestroySocketPendingUids.size(); i++) {
+            uids.add(mDestroySocketPendingUids.keyAt(i));
+        }
+
+        try {
+            mDeps.destroyLiveTcpSocketsByOwnerUids(uids);
+        } catch (SocketException | InterruptedIOException | ErrnoException e) {
+            loge("Failed to destroy sockets: " + e);
+        }
+        mDestroySocketPendingUids.clear();
     }
 
     private void handleReportNetworkActivity(final NetworkActivityParams params) {
@@ -3369,43 +3628,46 @@
             isCellNetworkActivity = params.label == TRANSPORT_CELLULAR;
         }
 
-        if (mDelayDestroyFrozenSockets
-                && params.isActive
-                && isCellNetworkActivity
-                && !mPendingFrozenUids.isEmpty()) {
-            closePendingFrozenSockets();
+        if (mDelayDestroySockets && params.isActive && isCellNetworkActivity) {
+            destroyPendingSockets();
         }
     }
 
     /**
-     * If the cellular network is no longer the default network, close pending frozen sockets.
+     * If the cellular network is no longer the default network, destroy pending sockets.
      *
      * @param newNetwork new default network
      * @param oldNetwork old default network
      */
-    private void maybeClosePendingFrozenSockets(NetworkAgentInfo newNetwork,
+    private void maybeDestroyPendingSockets(NetworkAgentInfo newNetwork,
             NetworkAgentInfo oldNetwork) {
         final boolean isOldNetworkCellular = oldNetwork != null
                 && oldNetwork.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
         final boolean isNewNetworkCellular = newNetwork != null
                 && newNetwork.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
 
-        if (isOldNetworkCellular
-                && !isNewNetworkCellular
-                && !mPendingFrozenUids.isEmpty()) {
-            closePendingFrozenSockets();
+        if (isOldNetworkCellular && !isNewNetworkCellular) {
+            destroyPendingSockets();
         }
     }
 
-    private void dumpCloseFrozenAppSockets(IndentingPrintWriter pw) {
-        pw.println("CloseFrozenAppSockets:");
+    private void dumpDestroySockets(IndentingPrintWriter pw) {
+        pw.println("DestroySockets:");
         pw.increaseIndent();
         pw.print("mDestroyFrozenSockets="); pw.println(mDestroyFrozenSockets);
-        pw.print("mDelayDestroyFrozenSockets="); pw.println(mDelayDestroyFrozenSockets);
-        pw.print("mPendingFrozenUids="); pw.println(mPendingFrozenUids);
+        pw.print("mDelayDestroySockets="); pw.println(mDelayDestroySockets);
+        pw.print("mDestroySocketPendingUids:");
+        pw.increaseIndent();
+        for (int i = 0; i < mDestroySocketPendingUids.size(); i++) {
+            final int uid = mDestroySocketPendingUids.keyAt(i);
+            final int reasons = mDestroySocketPendingUids.valueAt(i);
+            pw.print(uid + ": reasons=" + reasons);
+        }
+        pw.decreaseIndent();
         pw.decreaseIndent();
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private void dumpBpfProgramStatus(IndentingPrintWriter pw) {
         pw.println("Bpf Program Status:");
         pw.increaseIndent();
@@ -3414,12 +3676,37 @@
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_INGRESS));
             pw.print("CGROUP_INET_EGRESS: ");
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_EGRESS));
+
             pw.print("CGROUP_INET_SOCK_CREATE: ");
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_SOCK_CREATE));
+
             pw.print("CGROUP_INET4_BIND: ");
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET4_BIND));
             pw.print("CGROUP_INET6_BIND: ");
             pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET6_BIND));
+
+            pw.print("CGROUP_INET4_CONNECT: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET4_CONNECT));
+            pw.print("CGROUP_INET6_CONNECT: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET6_CONNECT));
+
+            pw.print("CGROUP_UDP4_SENDMSG: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_UDP4_SENDMSG));
+            pw.print("CGROUP_UDP6_SENDMSG: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_UDP6_SENDMSG));
+
+            pw.print("CGROUP_UDP4_RECVMSG: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_UDP4_RECVMSG));
+            pw.print("CGROUP_UDP6_RECVMSG: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_UDP6_RECVMSG));
+
+            pw.print("CGROUP_GETSOCKOPT: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_GETSOCKOPT));
+            pw.print("CGROUP_SETSOCKOPT: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_SETSOCKOPT));
+
+            pw.print("CGROUP_INET_SOCK_RELEASE: ");
+            pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_SOCK_RELEASE));
         } catch (IOException e) {
             pw.println("  IOException");
         }
@@ -3428,16 +3715,11 @@
 
     @VisibleForTesting
     static final String KEY_DESTROY_FROZEN_SOCKETS_VERSION = "destroy_frozen_sockets_version";
-    @VisibleForTesting
-    static final String DELAY_DESTROY_FROZEN_SOCKETS_VERSION =
-            "delay_destroy_frozen_sockets_version";
 
     @VisibleForTesting
     public static final String ALLOW_SYSUI_CONNECTIVITY_REPORTS =
             "allow_sysui_connectivity_reports";
 
-    public static final String LOG_BPF_RC = "log_bpf_rc_force_disable";
-
     public static final String ALLOW_SATALLITE_NETWORK_FALLBACK =
             "allow_satallite_network_fallback";
 
@@ -3811,6 +4093,10 @@
             mSatelliteAccessController.start();
         }
 
+        if (mCarrierPrivilegeAuthenticator != null) {
+            mCarrierPrivilegeAuthenticator.start();
+        }
+
         // On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
         if (mDeps.isAtLeastT()) {
             mBpfNetMaps.setPullAtomCallback(mContext);
@@ -3989,6 +4275,12 @@
             boolean verbose = !CollectionUtils.contains(args, SHORT_ARG);
             dumpTrafficController(pw, fd, verbose);
             return;
+        } else if (CollectionUtils.contains(args, CLATEGRESS4RAWBPFMAP_ARG)) {
+            dumpClatBpfRawMap(pw, true /* isEgress4Map */);
+            return;
+        } else if (CollectionUtils.contains(args, CLATINGRESS6RAWBPFMAP_ARG)) {
+            dumpClatBpfRawMap(pw, false /* isEgress4Map */);
+            return;
         }
 
         pw.println("NetworkProviders for:");
@@ -4062,10 +4354,21 @@
         dumpAvoidBadWifiSettings(pw);
 
         pw.println();
-        dumpCloseFrozenAppSockets(pw);
+        dumpDestroySockets(pw);
 
-        pw.println();
-        dumpBpfProgramStatus(pw);
+        if (mDeps.isAtLeastT()) {
+            // R: https://android.googlesource.com/platform/system/core/+/refs/heads/android11-release/rootdir/init.rc
+            //   shows /dev/cg2_bpf
+            // S: https://android.googlesource.com/platform/system/core/+/refs/heads/android12-release/rootdir/init.rc
+            //   does not
+            // Thus cgroups are mounted at /dev/cg2_bpf on R and not on /sys/fs/cgroup
+            // so the following won't work (on R) anyway.
+            // The /sys/fs/cgroup path is only actually enforced/required starting with U,
+            // but it is very likely to already be the case (though not guaranteed) on T.
+            // I'm not at all sure about S - let's just skip it to get rid of lint warnings.
+            pw.println();
+            dumpBpfProgramStatus(pw);
+        }
 
         if (null != mCarrierPrivilegeAuthenticator) {
             pw.println();
@@ -4136,6 +4439,9 @@
         pw.println();
         pw.println("Multicast routing supported: " +
                 (mMulticastRoutingCoordinatorService != null));
+
+        pw.println();
+        pw.println("Background firewall chain enabled: " + mBackgroundFirewallChainEnabled);
     }
 
     private void dumpNetworks(IndentingPrintWriter pw) {
@@ -4245,6 +4551,15 @@
         }
     }
 
+    private void dumpClatBpfRawMap(IndentingPrintWriter pw, boolean isEgress4Map) {
+        for (NetworkAgentInfo nai : networksSortedById()) {
+            if (nai.clatd != null) {
+                nai.clatd.dumpRawBpfMap(pw, isEgress4Map);
+                break;
+            }
+        }
+    }
+
     private void dumpAllRequestInfoLogsToLogcat() {
         try (PrintWriter logPw = new PrintWriter(new Writer() {
             @Override
@@ -4622,7 +4937,15 @@
                 final String logMsg = !TextUtils.isEmpty(redirectUrl)
                         ? " with redirect to " + redirectUrl
                         : "";
-                log(nai.toShortString() + " validation " + (valid ? "passed" : "failed") + logMsg);
+                final String statusMsg;
+                if (valid) {
+                    statusMsg = "passed";
+                } else if (!TextUtils.isEmpty(redirectUrl)) {
+                    statusMsg = "detected a portal";
+                } else {
+                    statusMsg = "failed";
+                }
+                log(nai.toShortString() + " validation " + statusMsg + logMsg);
             }
             if (valid != wasValidated) {
                 final FullScore oldScore = nai.getScore();
@@ -5134,7 +5457,7 @@
         // by other networks that are already connected. Perhaps that can be done by
         // sending all CALLBACK_LOST messages (for requests, not listens) at the end
         // of rematchAllNetworksAndRequests
-        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST);
+        notifyNetworkCallbacks(nai, CALLBACK_LOST);
         mKeepaliveTracker.handleStopAllKeepalives(nai, SocketKeepalive.ERROR_INVALID_NETWORK);
 
         mQosCallbackTracker.handleNetworkReleased(nai.network);
@@ -5172,7 +5495,7 @@
 
                 if (mDefaultRequest == nri) {
                     mNetworkActivityTracker.updateDefaultNetwork(null /* newNetwork */, nai);
-                    maybeClosePendingFrozenSockets(null /* newNetwork */, nai);
+                    maybeDestroyPendingSockets(null /* newNetwork */, nai);
                     ensureNetworkTransitionWakelock(nai.toShortString());
                 }
             }
@@ -5256,8 +5579,7 @@
             // correctly contains null as an upstream.
             if (sendCallbacks) {
                 nri.setSatisfier(null, null);
-                notifyNetworkCallbacks(local,
-                        ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+                notifyNetworkCallbacks(local, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
             }
         }
 
@@ -5332,6 +5654,7 @@
         // was is being disconnected the callbacks have already been sent, and if it is being
         // destroyed pending replacement they will be sent when it is disconnected.
         maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
+        updateIngressToVpnAddressFiltering(null, nai.linkProperties, nai);
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -5448,6 +5771,12 @@
                 // If there is an active request, then for sure there is a satisfier.
                 nri.getSatisfier().addRequest(activeRequest);
             }
+
+            if (shouldTrackUidsForBlockedStatusCallbacks()
+                    && isAppRequest(nri)
+                    && !nri.mUidTrackedForBlockedStatus) {
+                Log.wtf(TAG, "Registered nri is not tracked for sending blocked status: " + nri);
+            }
         }
 
         if (mFlags.noRematchAllRequestsOnRegister()) {
@@ -5625,8 +5954,7 @@
             log("releasing " + nri.mRequests.get(0) + " (timeout)");
         }
         handleRemoveNetworkRequest(nri);
-        callCallbackForRequest(
-                nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0);
+        callCallbackForRequest(nri, null, CALLBACK_UNAVAIL, 0);
     }
 
     private void handleReleaseNetworkRequest(@NonNull final NetworkRequest request,
@@ -5642,11 +5970,16 @@
         }
         handleRemoveNetworkRequest(nri);
         if (callOnUnavailable) {
-            callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0);
+            callCallbackForRequest(nri, null, CALLBACK_UNAVAIL, 0);
         }
     }
 
     private void handleRemoveNetworkRequest(@NonNull final NetworkRequestInfo nri) {
+        handleRemoveNetworkRequest(nri, true /* untrackUids */);
+    }
+
+    private void handleRemoveNetworkRequest(@NonNull final NetworkRequestInfo nri,
+            final boolean untrackUids) {
         ensureRunningOnConnectivityServiceThread();
         for (final NetworkRequest req : nri.mRequests) {
             if (null == mNetworkRequests.remove(req)) {
@@ -5681,7 +6014,9 @@
             }
         }
 
-        nri.mPerUidCounter.decrementCount(nri.mUid);
+        if (untrackUids) {
+            maybeUntrackUidAndClearBlockedReasons(nri);
+        }
         mNetworkRequestInfoLogs.log("RELEASE " + nri);
         checkNrisConsistency(nri);
 
@@ -5703,12 +6038,17 @@
     }
 
     private void handleRemoveNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
+        handleRemoveNetworkRequests(nris, true /* untrackUids */);
+    }
+
+    private void handleRemoveNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris,
+            final boolean untrackUids) {
         for (final NetworkRequestInfo nri : nris) {
             if (mDefaultRequest == nri) {
                 // Make sure we never remove the default request.
                 continue;
             }
-            handleRemoveNetworkRequest(nri);
+            handleRemoveNetworkRequest(nri, untrackUids);
         }
     }
 
@@ -6009,7 +6349,15 @@
             if (nm == null) return;
 
             if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
-                enforceNetworkStackPermission(mContext);
+                // This enforceNetworkStackPermission() should be adopted to check
+                // the required permission but this may be break OEM captive portal
+                // apps. Simply ignore the request if the caller does not have
+                // permission.
+                if (!hasNetworkStackPermission()) {
+                    Log.e(TAG, "Calling appRequest() without proper permission. Skip");
+                    return;
+                }
+
                 nm.forceReevaluation(mDeps.getCallingUid());
             }
         }
@@ -6440,8 +6788,8 @@
                     handlePrivateDnsValidationUpdate(
                             (PrivateDnsValidationUpdate) msg.obj);
                     break;
-                case EVENT_UID_BLOCKED_REASON_CHANGED:
-                    handleUidBlockedReasonChanged(msg.arg1, msg.arg2);
+                case EVENT_BLOCKED_REASONS_CHANGED:
+                    handleBlockedReasonsChanged((List) msg.obj);
                     break;
                 case EVENT_SET_REQUIRE_VPN_FOR_UIDS:
                     handleSetRequireVpnForUids(toBool(msg.arg1), (UidRange[]) msg.obj);
@@ -6490,8 +6838,11 @@
                     UidFrozenStateChangedArgs args = (UidFrozenStateChangedArgs) msg.obj;
                     handleFrozenUids(args.mUids, args.mFrozenStates);
                     break;
-                case EVENT_UID_CARRIER_PRIVILEGES_LOST:
-                    handleUidCarrierPrivilegesLost(msg.arg1, msg.arg2);
+                case EVENT_UPDATE_FIREWALL_DESTROY_SOCKET_REASONS:
+                    handleUpdateFirewallDestroySocketReasons((List) msg.obj);
+                    break;
+                case EVENT_CLEAR_FIREWALL_DESTROY_SOCKET_REASONS:
+                    handleClearFirewallDestroySocketReasons(msg.arg1);
                     break;
             }
         }
@@ -6775,7 +7126,7 @@
         // should have its link properties fixed up for PAC proxies.
         mProxyTracker.updateDefaultNetworkProxyPortForPAC(nai.linkProperties, nai.network);
         if (nai.everConnected()) {
-            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_IP_CHANGED);
+            notifyNetworkCallbacks(nai, CALLBACK_IP_CHANGED);
         }
     }
 
@@ -7261,6 +7612,29 @@
         // single NetworkRequest in mRequests.
         final List<NetworkRequest> mRequests;
 
+        /**
+         * List of callbacks that are queued for sending later when the requesting app is unfrozen.
+         *
+         * <p>There may typically be hundreds of NetworkRequestInfo, so a memory-efficient structure
+         * (just an int[]) is used to keep queued callbacks. This reduces the number of object
+         * references.
+         *
+         * <p>This is intended to be used with {@link CallbackQueue} which defines the internal
+         * format.
+         */
+        @NonNull
+        private int[] mQueuedCallbacks = new int[0];
+
+        private static final int MATCHED_NETID_NOT_FROZEN = -1;
+
+        /**
+         * If this request was already satisfied by a network when the requesting UID was frozen,
+         * the netId that was matched at that time. Otherwise, NETID_UNSET if no network was
+         * satisfying this request when frozen (including if this is a listen and not a request),
+         * and MATCHED_NETID_NOT_FROZEN if not frozen.
+         */
+        private int mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
+
         // mSatisfier and mActiveRequest rely on one another therefore set them together.
         void setSatisfier(
                 @Nullable final NetworkAgentInfo satisfier,
@@ -7309,9 +7683,16 @@
         // maximum limit of registered callbacks per UID.
         final int mAsUid;
 
+        // Flag to indicate that uid of this nri is tracked for sending blocked status callbacks.
+        // It is always true on V+ if mMessenger != null. As such, it's not strictly necessary.
+        // it's used only as a safeguard to avoid double counting or leaking.
+        boolean mUidTrackedForBlockedStatus;
+
         // Preference order of this request.
         final int mPreferenceOrder;
 
+        final int mDeclaredMethodsFlags;
+
         // In order to preserve the mapping of NetworkRequest-to-callback when apps register
         // callbacks using a returned NetworkRequest, the original NetworkRequest needs to be
         // maintained for keying off of. This is only a concern when the original nri
@@ -7358,7 +7739,6 @@
             mUid = mDeps.getCallingUid();
             mAsUid = asUid;
             mPerUidCounter = getRequestCounter(this);
-            mPerUidCounter.incrementCountOrThrow(mUid);
             /**
              * Location sensitive data not included in pending intent. Only included in
              * {@link NetworkCallback}.
@@ -7366,21 +7746,22 @@
             mCallbackFlags = NetworkCallback.FLAG_NONE;
             mCallingAttributionTag = callingAttributionTag;
             mPreferenceOrder = preferenceOrder;
+            mDeclaredMethodsFlags = DECLARED_METHODS_NONE;
         }
 
         NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r, @Nullable final Messenger m,
                 @Nullable final IBinder binder,
                 @NetworkCallback.Flag int callbackFlags,
-                @Nullable String callingAttributionTag) {
+                @Nullable String callingAttributionTag, int declaredMethodsFlags) {
             this(asUid, Collections.singletonList(r), r, m, binder, callbackFlags,
-                    callingAttributionTag);
+                    callingAttributionTag, declaredMethodsFlags);
         }
 
         NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
                 @NonNull final NetworkRequest requestForCallback, @Nullable final Messenger m,
                 @Nullable final IBinder binder,
                 @NetworkCallback.Flag int callbackFlags,
-                @Nullable String callingAttributionTag) {
+                @Nullable String callingAttributionTag, int declaredMethodsFlags) {
             super();
             ensureAllNetworkRequestsHaveType(r);
             mRequests = initializeRequests(r);
@@ -7392,10 +7773,10 @@
             mAsUid = asUid;
             mPendingIntent = null;
             mPerUidCounter = getRequestCounter(this);
-            mPerUidCounter.incrementCountOrThrow(mUid);
             mCallbackFlags = callbackFlags;
             mCallingAttributionTag = callingAttributionTag;
             mPreferenceOrder = PREFERENCE_ORDER_INVALID;
+            mDeclaredMethodsFlags = declaredMethodsFlags;
             linkDeathRecipient();
         }
 
@@ -7425,6 +7806,8 @@
                 }
                 setSatisfier(satisfier, activeRequest);
             }
+            mMatchedNetIdWhenFrozen = nri.mMatchedNetIdWhenFrozen;
+            mQueuedCallbacks = nri.mQueuedCallbacks;
             mMessenger = nri.mMessenger;
             mBinder = nri.mBinder;
             mPid = nri.mPid;
@@ -7432,10 +7815,11 @@
             mAsUid = nri.mAsUid;
             mPendingIntent = nri.mPendingIntent;
             mPerUidCounter = nri.mPerUidCounter;
-            mPerUidCounter.incrementCountOrThrow(mUid);
             mCallbackFlags = nri.mCallbackFlags;
             mCallingAttributionTag = nri.mCallingAttributionTag;
+            mUidTrackedForBlockedStatus = nri.mUidTrackedForBlockedStatus;
             mPreferenceOrder = PREFERENCE_ORDER_INVALID;
+            mDeclaredMethodsFlags = nri.mDeclaredMethodsFlags;
             linkDeathRecipient();
         }
 
@@ -7488,6 +7872,190 @@
             }
         }
 
+        /**
+         * Called when this NRI is being frozen.
+         *
+         * <p>Calling this method multiple times when the NRI is frozen is fine. This may happen
+         * if iterating through the NetworkRequest -> NRI map since there are duplicates in the
+         * NRI values for multilayer requests. It may also happen if an app is frozen, killed,
+         * restarted and refrozen since there is no callback sent when processes are killed, but in
+         * that case the callbacks to the killed app do not matter.
+         */
+        void onFrozen() {
+            if (mMatchedNetIdWhenFrozen != MATCHED_NETID_NOT_FROZEN) {
+                // Already frozen
+                return;
+            }
+            if (mSatisfier != null) {
+                mMatchedNetIdWhenFrozen = mSatisfier.network.netId;
+            } else {
+                mMatchedNetIdWhenFrozen = NETID_UNSET;
+            }
+        }
+
+        boolean maybeQueueCallback(@NonNull NetworkAgentInfo nai, int callbackId) {
+            if (mMatchedNetIdWhenFrozen == MATCHED_NETID_NOT_FROZEN) {
+                return false;
+            }
+
+            boolean ignoreThisCallback = false;
+            final int netId = nai.network.netId;
+            final CallbackQueue queue = new CallbackQueue(mQueuedCallbacks);
+            // Based on the new callback, clear previous callbacks that are no longer necessary.
+            // For example, if the network is lost, there is no need to send intermediate callbacks.
+            switch (callbackId) {
+                // PRECHECK is not an API and not very meaningful, do not deliver it for frozen apps
+                // Networks are likely to already be lost when the app is unfrozen, also skip LOSING
+                case CALLBACK_PRECHECK:
+                case CALLBACK_LOSING:
+                    ignoreThisCallback = true;
+                    break;
+                case CALLBACK_LOST:
+                    // All callbacks for this netId before onLost are unnecessary. And onLost itself
+                    // is also unnecessary if onAvailable was previously queued for this netId: the
+                    // Network just appeared and disappeared while the app was frozen.
+                    ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
+                    queue.removeCallbacksForNetId(netId);
+                    break;
+                case CALLBACK_AVAILABLE:
+                    if (mSatisfier != null) {
+                        // For requests that are satisfied by individual networks (not LISTEN), when
+                        // AVAILABLE is received, the request is matching a new Network, so previous
+                        // callbacks (for other Networks) are unnecessary.
+                        queue.clear();
+                    }
+                    break;
+                case CALLBACK_SUSPENDED:
+                case CALLBACK_RESUMED:
+                    if (queue.hasCallback(netId, CALLBACK_AVAILABLE)) {
+                        // AVAILABLE will already send the latest suspended status
+                        ignoreThisCallback = true;
+                        break;
+                    }
+                    // If SUSPENDED was queued, just remove it from the queue instead of sending
+                    // RESUMED; and vice-versa.
+                    final int otherCb = callbackId == CALLBACK_SUSPENDED
+                            ? CALLBACK_RESUMED
+                            : CALLBACK_SUSPENDED;
+                    ignoreThisCallback = queue.removeCallbacks(netId, otherCb);
+                    break;
+                case CALLBACK_CAP_CHANGED:
+                case CALLBACK_IP_CHANGED:
+                case CALLBACK_LOCAL_NETWORK_INFO_CHANGED:
+                case CALLBACK_BLK_CHANGED:
+                    ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
+                    break;
+                default:
+                    Log.wtf(TAG, "Unexpected callback type: "
+                            + ConnectivityManager.getCallbackName(callbackId));
+                    return false;
+            }
+
+            if (!ignoreThisCallback) {
+                // For non-listen (matching) callbacks, AVAILABLE can appear in the queue twice in a
+                // row for the same network if the new AVAILABLE suppressed intermediate AVAILABLEs
+                // for other networks. Example:
+                // A is matched, app is frozen, B is matched, A is matched again (removes callbacks
+                // for B), app is unfrozen.
+                // In that case call AVAILABLE sub-callbacks to update state, but not AVAILABLE
+                // itself.
+                if (callbackId == CALLBACK_AVAILABLE && netId == mMatchedNetIdWhenFrozen) {
+                    // The queue should have been cleared here, since this is AVAILABLE on a
+                    // non-listen callback (mMatchedNetIdWhenFrozen is set).
+                    addAvailableSubCallbacks(nai, queue);
+                } else {
+                    // When unfreezing, no need to send a callback multiple times for the same netId
+                    queue.removeCallbacks(netId, callbackId);
+                    // TODO: this code always adds the callback for simplicity. It would save
+                    // some CPU/memory if the code instead only added to the queue callbacks where
+                    // isCallbackOverridden=true, or which need to be in the queue because they
+                    // affect other callbacks that are overridden.
+                    queue.addCallback(netId, callbackId);
+                }
+            }
+            // Instead of shrinking the queue, possibly reallocating, the NRI could keep the array
+            // and length in memory for future adds, but this saves memory by avoiding the cost
+            // of an extra member and of unused array length (there are often hundreds of NRIs).
+            mQueuedCallbacks = queue.getMinimizedBackingArray();
+            return true;
+        }
+
+        /**
+         * Called when this NRI is being unfrozen to stop queueing, and send queued callbacks.
+         *
+         * <p>Calling this method multiple times when the NRI is unfrozen (for example iterating
+         * through the NetworkRequest -> NRI map where there are duplicate values for multilayer
+         * requests) is fine.
+         */
+        void sendQueuedCallbacks() {
+            mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
+            if (mQueuedCallbacks.length == 0) {
+                return;
+            }
+            new CallbackQueue(mQueuedCallbacks).forEach((netId, callbackId) -> {
+                // For CALLBACK_LOST only, there will not be a NAI for the netId. Build and send the
+                // callback directly.
+                if (callbackId == CALLBACK_LOST) {
+                    if (isCallbackOverridden(CALLBACK_LOST)) {
+                        final Bundle cbBundle = makeCommonBundleForCallback(this,
+                                new Network(netId));
+                        callCallbackForRequest(this, CALLBACK_LOST, cbBundle, 0 /* arg1 */);
+                    }
+                    return; // Next item in forEach
+                }
+
+                // Other callbacks should always have a NAI, because if a Network disconnects
+                // LOST will be called, unless the request is no longer satisfied by that Network in
+                // which case AVAILABLE will have been called for another Network. In both cases
+                // previous callbacks are cleared.
+                final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+                if (nai == null) {
+                    Log.wtf(TAG, "Missing NetworkAgentInfo for net " + netId
+                            + " for callback " + callbackId);
+                    return; // Next item in forEach
+                }
+
+                final int arg1 =
+                        callbackId == CALLBACK_AVAILABLE || callbackId == CALLBACK_BLK_CHANGED
+                                ? getBlockedState(nai, mAsUid)
+                                : 0;
+                callCallbackForRequest(this, nai, callbackId, arg1);
+            });
+            mQueuedCallbacks = new int[0];
+        }
+
+        boolean isCallbackOverridden(int callbackId) {
+            return !mUseDeclaredMethodsForCallbacksEnabled
+                    || (mDeclaredMethodsFlags & (1 << callbackId)) != 0;
+        }
+
+        /**
+         * Queue all callbacks that are called by AVAILABLE, except onAvailable.
+         *
+         * <p>AVAILABLE may call SUSPENDED, CAP_CHANGED, IP_CHANGED, LOCAL_NETWORK_INFO_CHANGED,
+         * and BLK_CHANGED, in this order.
+         */
+        private void addAvailableSubCallbacks(
+                @NonNull NetworkAgentInfo nai, @NonNull CallbackQueue queue) {
+            final boolean callSuspended =
+                    !nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+            final boolean callLocalInfoChanged = nai.isLocalNetwork();
+
+            final int cbCount = 3 + (callSuspended ? 1 : 0) + (callLocalInfoChanged ? 1 : 0);
+            // Avoid unnecessary re-allocations by reserving enough space for all callbacks to add.
+            queue.ensureHasCapacity(cbCount);
+            final int netId = nai.network.netId;
+            if (callSuspended) {
+                queue.addCallback(netId, CALLBACK_SUSPENDED);
+            }
+            queue.addCallback(netId, CALLBACK_CAP_CHANGED);
+            queue.addCallback(netId, CALLBACK_IP_CHANGED);
+            if (callLocalInfoChanged) {
+                queue.addCallback(netId, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+            }
+            queue.addCallback(netId, CALLBACK_BLK_CHANGED);
+        }
+
         boolean hasHigherOrderThan(@NonNull final NetworkRequestInfo target) {
             // Compare two preference orders.
             return mPreferenceOrder < target.mPreferenceOrder;
@@ -7522,10 +8090,54 @@
                     + " " + mRequests
                     + (mPendingIntent == null ? "" : " to trigger " + mPendingIntent)
                     + " callback flags: " + mCallbackFlags
-                    + " order: " + mPreferenceOrder;
+                    + " order: " + mPreferenceOrder
+                    + " isUidTracked: " + mUidTrackedForBlockedStatus
+                    + " declaredMethods: " + declaredMethodsFlagsToString(mDeclaredMethodsFlags);
         }
     }
 
+    /**
+     * Get a readable String for a bitmask of declared methods.
+     */
+    @VisibleForTesting
+    public static String declaredMethodsFlagsToString(int flags) {
+        if (flags == DECLARED_METHODS_NONE) {
+            return "NONE";
+        }
+        if (flags == DECLARED_METHODS_ALL) {
+            return "ALL";
+        }
+        final StringBuilder sb = new StringBuilder();
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_PRECHECK, "PRECHK", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_AVAILABLE, "AVAIL", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOSING, "LOSING", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOST, "LOST", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_UNAVAIL, "UNAVAIL", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_CAP_CHANGED, "NC", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_IP_CHANGED, "LP", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_SUSPENDED, "SUSP", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_RESUMED, "RESUME", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_BLK_CHANGED, "BLK", sb);
+        flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOCAL_NETWORK_INFO_CHANGED,
+                "LOCALINF", sb);
+        if (flags != 0) {
+            sb.append("|0x").append(Integer.toHexString(flags));
+        }
+        return sb.toString();
+    }
+
+    private static int maybeAppendDeclaredMethod(int declaredMethodsFlags,
+            int callbackId, String callbackName, @NonNull StringBuilder builder) {
+        final int callbackFlag = 1 << callbackId;
+        if ((declaredMethodsFlags & callbackFlag) != 0) {
+            if (builder.length() > 0) {
+                builder.append('|');
+            }
+            builder.append(callbackName);
+        }
+        return declaredMethodsFlags & ~callbackFlag;
+    }
+
     // Keep backward compatibility since the ServiceSpecificException is used by
     // the API surface, see {@link ConnectivityManager#convertServiceException}.
     public static class RequestInfoPerUidCounter extends PerUidCounter {
@@ -7572,15 +8184,6 @@
                     "Insufficient permissions to request a specific signal strength");
         }
         mAppOpsManager.checkPackage(callerUid, callerPackageName);
-
-        if (nc.getSubscriptionIds().isEmpty()) {
-            return;
-        }
-        if (mRequestRestrictedWifiEnabled
-                && canRequestRestrictedNetworkDueToCarrierPrivileges(nc, callerUid)) {
-            return;
-        }
-        enforceNetworkFactoryPermission();
     }
 
     private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) {
@@ -7669,7 +8272,21 @@
     public NetworkRequest requestNetwork(int asUid, NetworkCapabilities networkCapabilities,
             int reqTypeInt, Messenger messenger, int timeoutMs, final IBinder binder,
             int legacyType, int callbackFlags, @NonNull String callingPackageName,
-            @Nullable String callingAttributionTag) {
+            @Nullable String callingAttributionTag, int declaredMethodsFlag) {
+        if (declaredMethodsFlag == 0) {
+            // This could happen if raw binder calls are used to call the previous overload of
+            // requestNetwork, as missing int arguments in a binder call end up as 0
+            // (Parcel.readInt returns 0 at the end of a parcel). Such raw calls this would be
+            // really unexpected bad behavior from the caller though.
+            // TODO: remove after verifying this does not happen. This could allow enabling the
+            // optimization for callbacks that do not override any method (right now they use
+            // DECLARED_METHODS_ALL), if it is OK to break NetworkCallbacks created using
+            // dexmaker-mockito-inline and either spy() or MockSettings.useConstructor (see
+            // comment in ConnectivityManager which sets the flag to DECLARED_METHODS_ALL).
+            Log.wtf(TAG, "requestNetwork called without declaredMethodsFlag from "
+                    + callingPackageName);
+            declaredMethodsFlag = DECLARED_METHODS_ALL;
+        }
         if (legacyType != TYPE_NONE && !hasNetworkStackPermission()) {
             if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(),
                     callingPackageName)) {
@@ -7714,10 +8331,12 @@
                 //  the state of the app when the request is filed, but we never change the
                 //  request if the app changes network state. http://b/29964605
                 enforceMeteredApnPolicy(networkCapabilities);
+                maybeDisableLocalNetworkMatching(networkCapabilities, callingUid);
                 break;
             case LISTEN_FOR_BEST:
                 enforceAccessPermission();
                 networkCapabilities = new NetworkCapabilities(networkCapabilities);
+                maybeDisableLocalNetworkMatching(networkCapabilities, callingUid);
                 break;
             default:
                 throw new IllegalArgumentException("Unsupported request type " + reqType);
@@ -7744,13 +8363,6 @@
             throw new IllegalArgumentException("Bad timeout specified");
         }
 
-        final NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, legacyType,
-                nextNetworkRequestId(), reqType);
-        final NetworkRequestInfo nri = getNriToRegister(
-                asUid, networkRequest, messenger, binder, callbackFlags,
-                callingAttributionTag);
-        if (DBG) log("requestNetwork for " + nri);
-
         // For TRACK_SYSTEM_DEFAULT callbacks, the capabilities have been modified since they were
         // copied from the default request above. (This is necessary to ensure, for example, that
         // the callback does not leak sensitive information to unprivileged apps.) Check that the
@@ -7762,7 +8374,13 @@
                     + networkCapabilities + " vs. " + defaultNc);
         }
 
-        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST, nri));
+        final NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, legacyType,
+                nextNetworkRequestId(), reqType);
+        final NetworkRequestInfo nri = getNriToRegister(
+                asUid, networkRequest, messenger, binder, callbackFlags,
+                callingAttributionTag, declaredMethodsFlag);
+        if (DBG) log("requestNetwork for " + nri);
+        trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_REQUEST, nri);
         if (timeoutMs > 0) {
             mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_TIMEOUT_NETWORK_REQUEST,
                     nri), timeoutMs);
@@ -7786,7 +8404,7 @@
     private NetworkRequestInfo getNriToRegister(final int asUid, @NonNull final NetworkRequest nr,
             @Nullable final Messenger msgr, @Nullable final IBinder binder,
             @NetworkCallback.Flag int callbackFlags,
-            @Nullable String callingAttributionTag) {
+            @Nullable String callingAttributionTag, int declaredMethodsFlags) {
         final List<NetworkRequest> requests;
         if (NetworkRequest.Type.TRACK_DEFAULT == nr.type) {
             requests = copyDefaultNetworkRequestsForUid(
@@ -7795,7 +8413,8 @@
             requests = Collections.singletonList(nr);
         }
         return new NetworkRequestInfo(
-                asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag);
+                asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag,
+                declaredMethodsFlags);
     }
 
     private boolean shouldCheckCapabilitiesDeclaration(
@@ -7804,7 +8423,7 @@
         final UserHandle user = UserHandle.getUserHandleForUid(callingUid);
         // Only run the check if the change is enabled.
         if (!mDeps.isChangeEnabled(
-                ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION,
+                ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION,
                 callingPackageName, user)) {
             return false;
         }
@@ -7932,14 +8551,13 @@
             // Policy already enforced.
             return;
         }
-        final long ident = Binder.clearCallingIdentity();
-        try {
-            if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {
-                // If UID is restricted, don't allow them to bring up metered APNs.
-                networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(ident);
+        final boolean isRestrictedOnMeteredNetworks = mDeps.isAtLeastV()
+                ? mBpfNetMaps.isUidRestrictedOnMeteredNetworks(uid)
+                : BinderUtils.withCleanCallingIdentity(() ->
+                        mPolicyManager.isUidRestrictedOnMeteredNetworks(uid));
+        if (isRestrictedOnMeteredNetworks) {
+            // If UID is restricted, don't allow them to bring up metered APNs.
+            networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
         }
     }
 
@@ -7956,16 +8574,15 @@
         ensureRequestableCapabilities(networkCapabilities);
         ensureSufficientPermissionsForRequest(networkCapabilities,
                 Binder.getCallingPid(), callingUid, callingPackageName);
-        restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities,
-                callingUid, callingPackageName);
+        restrictRequestNetworkCapabilitiesForCaller(
+                networkCapabilities, callingUid, callingPackageName);
 
         NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE,
                 nextNetworkRequestId(), NetworkRequest.Type.REQUEST);
         NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, networkRequest, operation,
                 callingAttributionTag);
         if (DBG) log("pendingRequest for " + nri);
-        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT,
-                nri));
+        trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT, nri);
         return networkRequest;
     }
 
@@ -8004,11 +8621,88 @@
         return true;
     }
 
+    private boolean isAppRequest(NetworkRequestInfo nri) {
+        return nri.mMessenger != null || nri.mPendingIntent != null;
+    }
+
+    private void trackUidAndMaybePostCurrentBlockedReason(final NetworkRequestInfo nri) {
+        if (!isAppRequest(nri)) {
+            Log.wtf(TAG, "trackUidAndMaybePostCurrentBlockedReason is called for non app"
+                    + "request: " + nri);
+            return;
+        }
+        nri.mPerUidCounter.incrementCountOrThrow(nri.mUid);
+
+        // If nri.mMessenger is null, this nri does not have NetworkCallback so ConnectivityService
+        // does not need to send onBlockedStatusChanged callback for this uid and does not need to
+        // track the uid in mBlockedStatusTrackingUids
+        if (!shouldTrackUidsForBlockedStatusCallbacks() || nri.mMessenger == null) {
+            return;
+        }
+        if (nri.mUidTrackedForBlockedStatus) {
+            Log.wtf(TAG, "Nri is already tracked for sending blocked status: " + nri);
+            return;
+        }
+        nri.mUidTrackedForBlockedStatus = true;
+        synchronized (mBlockedStatusTrackingUids) {
+            final int uid = nri.mAsUid;
+            final int count = mBlockedStatusTrackingUids.get(uid, 0);
+            if (count == 0) {
+                mHandler.sendMessage(mHandler.obtainMessage(EVENT_BLOCKED_REASONS_CHANGED,
+                        List.of(new Pair<>(uid, mBpfNetMaps.getUidNetworkingBlockedReasons(uid)))));
+            }
+            mBlockedStatusTrackingUids.put(uid, count + 1);
+        }
+    }
+
+    private void trackUidAndRegisterNetworkRequest(final int event, NetworkRequestInfo nri) {
+        // Post the update of the UID's blocked reasons before posting the message that registers
+        // the callback. This is necessary because if the callback immediately matches a request,
+        // the onBlockedStatusChanged must be called with the correct blocked reasons.
+        // Also, once trackUidAndMaybePostCurrentBlockedReason is called, the register network
+        // request event must be posted, because otherwise the counter for uid will never be
+        // decremented.
+        trackUidAndMaybePostCurrentBlockedReason(nri);
+        mHandler.sendMessage(mHandler.obtainMessage(event, nri));
+    }
+
+    private void maybeUntrackUidAndClearBlockedReasons(final NetworkRequestInfo nri) {
+        if (!isAppRequest(nri)) {
+            // Not an app request.
+            return;
+        }
+        nri.mPerUidCounter.decrementCount(nri.mUid);
+
+        if (!shouldTrackUidsForBlockedStatusCallbacks() || nri.mMessenger == null) {
+            return;
+        }
+        if (!nri.mUidTrackedForBlockedStatus) {
+            Log.wtf(TAG, "Nri is not tracked for sending blocked status: " + nri);
+            return;
+        }
+        nri.mUidTrackedForBlockedStatus = false;
+        synchronized (mBlockedStatusTrackingUids) {
+            final int count = mBlockedStatusTrackingUids.get(nri.mAsUid);
+            if (count > 1) {
+                mBlockedStatusTrackingUids.put(nri.mAsUid, count - 1);
+            } else {
+                mBlockedStatusTrackingUids.delete(nri.mAsUid);
+                mUidBlockedReasons.delete(nri.mAsUid);
+            }
+        }
+    }
+
     @Override
     public NetworkRequest listenForNetwork(NetworkCapabilities networkCapabilities,
             Messenger messenger, IBinder binder,
             @NetworkCallback.Flag int callbackFlags,
-            @NonNull String callingPackageName, @NonNull String callingAttributionTag) {
+            @NonNull String callingPackageName, @NonNull String callingAttributionTag,
+            int declaredMethodsFlag) {
+        if (declaredMethodsFlag == 0) {
+            Log.wtf(TAG, "listenForNetwork called without declaredMethodsFlag from "
+                    + callingPackageName);
+            declaredMethodsFlag = DECLARED_METHODS_ALL;
+        }
         final int callingUid = mDeps.getCallingUid();
         if (!hasWifiNetworkListenPermission(networkCapabilities)) {
             enforceAccessPermission();
@@ -8017,7 +8711,7 @@
         NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
         ensureSufficientPermissionsForRequest(networkCapabilities,
                 Binder.getCallingPid(), callingUid, callingPackageName);
-        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+        restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
         // Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so
         // make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get
         // onLost and onAvailable callbacks when networks move in and out of the background.
@@ -8030,10 +8724,10 @@
                 NetworkRequest.Type.LISTEN);
         NetworkRequestInfo nri =
                 new NetworkRequestInfo(callingUid, networkRequest, messenger, binder, callbackFlags,
-                        callingAttributionTag);
+                        callingAttributionTag, declaredMethodsFlag);
         if (VDBG) log("listenForNetwork for " + nri);
 
-        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
+        trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_LISTENER, nri);
         return networkRequest;
     }
 
@@ -8050,7 +8744,7 @@
         ensureSufficientPermissionsForRequest(networkCapabilities,
                 Binder.getCallingPid(), callingUid, callingPackageName);
         final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
-        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+        restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
 
         NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
                 NetworkRequest.Type.LISTEN);
@@ -8058,8 +8752,7 @@
                 callingAttributionTag);
         if (VDBG) log("pendingListenForNetwork for " + nri);
 
-        mHandler.sendMessage(mHandler.obtainMessage(
-                    EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT, nri));
+        trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT, nri);
     }
 
     /** Returns the next Network provider ID. */
@@ -8681,6 +9374,8 @@
         // new interface (the interface name -> index map becomes initialized)
         updateVpnFiltering(newLp, oldLp, networkAgent);
 
+        updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
+
         updateMtu(newLp, oldLp);
         // TODO - figure out what to do for clat
 //        for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -8729,7 +9424,7 @@
             }
             networkAgent.networkMonitor().notifyLinkPropertiesChanged(
                     new LinkProperties(newLp, true /* parcelSensitiveFields */));
-            notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
+            notifyNetworkCallbacks(networkAgent, CALLBACK_IP_CHANGED);
         }
 
         mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent);
@@ -8972,6 +9667,92 @@
         }
     }
 
+    /**
+     * Returns ingress discard rules to drop packets to VPN addresses ingressing via non-VPN
+     * interfaces.
+     * Ingress discard rule is added to the address iff
+     *   1. The address is not a link local address
+     *   2. The address is used by a single interface of VPN whose VPN type is not TYPE_VPN_LEGACY
+     *      or TYPE_VPN_OEM and the address is not used by any other interfaces even non-VPN ones
+     * Ingress discard rule is not be added to TYPE_VPN_LEGACY or TYPE_VPN_OEM VPN since these VPNs
+     * might need to receive packet to VPN address via non-VPN interface.
+     * This method can be called during network disconnects, when nai has already been removed from
+     * mNetworkAgentInfos.
+     *
+     * @param nai This method generates rules assuming lp of this nai is the lp at the second
+     *            argument.
+     * @param lp  This method generates rules assuming lp of nai at the first argument is this lp.
+     *            Caller passes old lp to generate old rules and new lp to generate new rules.
+     * @return    ingress discard rules. Set of pairs of addresses and interface names
+     */
+    private Set<Pair<InetAddress, String>> generateIngressDiscardRules(
+            @NonNull final NetworkAgentInfo nai, @Nullable final LinkProperties lp) {
+        Set<NetworkAgentInfo> nais = new ArraySet<>(mNetworkAgentInfos);
+        nais.add(nai);
+        // Determine how many networks each IP address is currently configured on.
+        // Ingress rules are added only for IP addresses that are configured on single interface.
+        final Map<InetAddress, Integer> addressOwnerCounts = new ArrayMap<>();
+        for (final NetworkAgentInfo agent : nais) {
+            if (agent.isDestroyed()) {
+                continue;
+            }
+            final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
+            if (agentLp == null) {
+                continue;
+            }
+            for (final InetAddress addr: agentLp.getAllAddresses()) {
+                addressOwnerCounts.put(addr, addressOwnerCounts.getOrDefault(addr, 0) + 1);
+            }
+        }
+
+        // Iterates all networks instead of only generating rule for nai that was passed in since
+        // lp of the nai change could cause/resolve address collision and result in affecting rule
+        // for different network.
+        final Set<Pair<InetAddress, String>> ingressDiscardRules = new ArraySet<>();
+        for (final NetworkAgentInfo agent : nais) {
+            final int vpnType = getVpnType(agent);
+            if (!agent.isVPN() || agent.isDestroyed()
+                    || vpnType == VpnManager.TYPE_VPN_LEGACY
+                    || vpnType == VpnManager.TYPE_VPN_OEM) {
+                continue;
+            }
+            final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
+            if (agentLp == null || agentLp.getInterfaceName() == null) {
+                continue;
+            }
+
+            for (final InetAddress addr: agentLp.getAllAddresses()) {
+                if (addressOwnerCounts.get(addr) == 1 && !addr.isLinkLocalAddress()) {
+                    ingressDiscardRules.add(new Pair<>(addr, agentLp.getInterfaceName()));
+                }
+            }
+        }
+        return ingressDiscardRules;
+    }
+
+    private void updateIngressToVpnAddressFiltering(@Nullable LinkProperties newLp,
+            @Nullable LinkProperties oldLp, @NonNull NetworkAgentInfo nai) {
+        // Having isAtleastT to avoid NewApi linter error (b/303382209)
+        if (!mIngressToVpnAddressFiltering || !mDeps.isAtLeastT()) {
+            return;
+        }
+        final CompareOrUpdateResult<InetAddress, Pair<InetAddress, String>> ruleDiff =
+                new CompareOrUpdateResult<>(
+                        generateIngressDiscardRules(nai, oldLp),
+                        generateIngressDiscardRules(nai, newLp),
+                        (rule) -> rule.first);
+        for (Pair<InetAddress, String> rule: ruleDiff.removed) {
+            mBpfNetMaps.removeIngressDiscardRule(rule.first);
+        }
+        for (Pair<InetAddress, String> rule: ruleDiff.added) {
+            mBpfNetMaps.setIngressDiscardRule(rule.first, rule.second);
+        }
+        // setIngressDiscardRule overrides the existing rule
+        for (Pair<InetAddress, String> rule: ruleDiff.updated) {
+            mBpfNetMaps.setIngressDiscardRule(rule.first, rule.second);
+        }
+    }
+
     private void updateWakeOnLan(@NonNull LinkProperties lp) {
         if (mWolSupportedInterfaces == null) {
             mWolSupportedInterfaces = new ArraySet<>(mResources.get().getStringArray(
@@ -9144,8 +9925,7 @@
         if (prevSuspended != suspended) {
             // TODO (b/73132094) : remove this call once the few users of onSuspended and
             // onResumed have been removed.
-            notifyNetworkCallbacks(nai, suspended ? ConnectivityManager.CALLBACK_SUSPENDED
-                    : ConnectivityManager.CALLBACK_RESUMED);
+            notifyNetworkCallbacks(nai, suspended ? CALLBACK_SUSPENDED : CALLBACK_RESUMED);
         }
         if (prevSuspended != suspended || prevRoaming != roaming) {
             // updateNetworkInfo will mix in the suspended info from the capabilities and
@@ -9155,6 +9935,9 @@
     }
 
     private void handleUidCarrierPrivilegesLost(int uid, int subId) {
+        if (!mRequestRestrictedWifiEnabled) {
+            return;
+        }
         ensureRunningOnConnectivityServiceThread();
         // A NetworkRequest needs to be revoked when all the conditions are met
         //   1. It requests restricted network
@@ -9162,7 +9945,7 @@
         //   3. The app doesn't have Carrier Privileges
         //   4. The app doesn't have permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
         for (final NetworkRequest nr : mNetworkRequests.keySet()) {
-            if ((nr.isRequest() || nr.isListen())
+            if (nr.isRequest()
                     && !nr.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
                     && nr.getRequestorUid() == uid
                     && getSubscriptionIdFromNetworkCaps(nr.networkCapabilities) == subId
@@ -9229,7 +10012,7 @@
             // If the requestable capabilities have changed or the score changed, we can't have been
             // called by rematchNetworkAndRequests, so it's safe to start a rematch.
             rematchAllNetworksAndRequests();
-            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+            notifyNetworkCallbacks(nai, CALLBACK_CAP_CHANGED);
         }
         updateNetworkInfoForRoamingAndSuspended(nai, prevNc, newNc);
 
@@ -9371,7 +10154,7 @@
                 // But here there is no new request, so the rematch won't see anything. Send
                 // callbacks to apps now to tell them about the loss of upstream.
                 notifyNetworkCallbacks(nai,
-                        ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+                        CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
                 return;
             }
         }
@@ -9387,7 +10170,8 @@
             configBuilder.setUpstreamSelector(nr);
             final NetworkRequestInfo nri = new NetworkRequestInfo(
                     nai.creatorUid, nr, null /* messenger */, null /* binder */,
-                    0 /* callbackFlags */, null /* attributionTag */);
+                    0 /* callbackFlags */, null /* attributionTag */,
+                    DECLARED_METHODS_NONE);
             if (null != oldSatisfier) {
                 // Set the old satisfier in the new NRI so that the rematch will see any changes
                 nri.setSatisfier(oldSatisfier, nr);
@@ -9676,7 +10460,7 @@
 
     private void sendPendingIntentForRequest(NetworkRequestInfo nri, NetworkAgentInfo networkAgent,
             int notificationType) {
-        if (notificationType == ConnectivityManager.CALLBACK_AVAILABLE && !nri.mPendingIntentSent) {
+        if (notificationType == CALLBACK_AVAILABLE && !nri.mPendingIntentSent) {
             Intent intent = new Intent();
             intent.putExtra(ConnectivityManager.EXTRA_NETWORK, networkAgent.network);
             // If apps could file multi-layer requests with PendingIntents, they'd need to know
@@ -9746,6 +10530,18 @@
         return new LocalNetworkInfo.Builder().setUpstreamNetwork(upstream).build();
     }
 
+    private Bundle makeCommonBundleForCallback(@NonNull final NetworkRequestInfo nri,
+            @Nullable Network network) {
+        final Bundle bundle = new Bundle();
+        // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
+        // TODO: check if defensive copies of data is needed.
+        putParcelable(bundle, nri.getNetworkRequestForCallback());
+        if (network != null) {
+            putParcelable(bundle, network);
+        }
+        return bundle;
+    }
+
     // networkAgent is only allowed to be null if notificationType is
     // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
     // available, while all other cases are about some particular network.
@@ -9758,19 +10554,24 @@
             // are Type.LISTEN, but should not have NetworkCallbacks invoked.
             return;
         }
-        final Bundle bundle = new Bundle();
-        // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
-        // TODO: check if defensive copies of data is needed.
-        final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
-        putParcelable(bundle, nrForCallback);
-        Message msg = Message.obtain();
-        if (notificationType != ConnectivityManager.CALLBACK_UNAVAIL) {
-            putParcelable(bundle, networkAgent.network);
+        // Even if a callback ends up not being sent, it may affect other callbacks in the queue, so
+        // queue callbacks before checking the declared methods flags.
+        if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) {
+            return;
         }
+        if (!nri.isCallbackOverridden(notificationType)) {
+            // No need to send the notification as the recipient method is not overridden
+            return;
+        }
+        final Network bundleNetwork = notificationType == CALLBACK_UNAVAIL
+                ? null
+                : networkAgent.network;
+        final Bundle bundle = makeCommonBundleForCallback(nri, bundleNetwork);
         final boolean includeLocationSensitiveInfo =
                 (nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
+        final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
         switch (notificationType) {
-            case ConnectivityManager.CALLBACK_AVAILABLE: {
+            case CALLBACK_AVAILABLE: {
                 final NetworkCapabilities nc =
                         createWithLocationInfoSanitizedIfNecessaryWhenParceled(
                                 networkCapabilitiesRestrictedForCallerPermissions(
@@ -9785,15 +10586,9 @@
                 // method here.
                 bundle.putParcelable(LocalNetworkInfo.class.getSimpleName(),
                         localNetworkInfoForNai(networkAgent));
-                // For this notification, arg1 contains the blocked status.
-                msg.arg1 = arg1;
                 break;
             }
-            case ConnectivityManager.CALLBACK_LOSING: {
-                msg.arg1 = arg1;
-                break;
-            }
-            case ConnectivityManager.CALLBACK_CAP_CHANGED: {
+            case CALLBACK_CAP_CHANGED: {
                 // networkAgent can't be null as it has been accessed a few lines above.
                 final NetworkCapabilities netCap =
                         networkCapabilitiesRestrictedForCallerPermissions(
@@ -9806,17 +10601,16 @@
                                 nri.mCallingAttributionTag));
                 break;
             }
-            case ConnectivityManager.CALLBACK_IP_CHANGED: {
+            case CALLBACK_IP_CHANGED: {
                 putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
                         networkAgent.linkProperties, nri.mPid, nri.mUid));
                 break;
             }
-            case ConnectivityManager.CALLBACK_BLK_CHANGED: {
+            case CALLBACK_BLK_CHANGED: {
                 maybeLogBlockedStatusChanged(nri, networkAgent.network, arg1);
-                msg.arg1 = arg1;
                 break;
             }
-            case ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+            case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
                 if (!networkAgent.isLocalNetwork()) {
                     Log.wtf(TAG, "Callback for local info for a non-local network");
                     return;
@@ -9825,17 +10619,26 @@
                 break;
             }
         }
+        callCallbackForRequest(nri, notificationType, bundle, arg1);
+    }
+
+    private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri, int notificationType,
+            Bundle bundle, int arg1) {
+        Message msg = Message.obtain();
+        msg.arg1 = arg1;
         msg.what = notificationType;
         msg.setData(bundle);
         try {
             if (VDBG) {
                 String notification = ConnectivityManager.getCallbackName(notificationType);
-                log("sending notification " + notification + " for " + nrForCallback);
+                log("sending notification " + notification + " for "
+                        + nri.getNetworkRequestForCallback());
             }
             nri.mMessenger.send(msg);
         } catch (RemoteException e) {
             // may occur naturally in the race of binder death.
-            loge("RemoteException caught trying to send a callback msg for " + nrForCallback);
+            loge("RemoteException caught trying to send a callback msg for "
+                    + nri.getNetworkRequestForCallback());
         }
     }
 
@@ -9956,7 +10759,7 @@
             mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
         }
         mNetworkActivityTracker.updateDefaultNetwork(newDefaultNetwork, oldDefaultNetwork);
-        maybeClosePendingFrozenSockets(newDefaultNetwork, oldDefaultNetwork);
+        maybeDestroyPendingSockets(newDefaultNetwork, oldDefaultNetwork);
         mProxyTracker.setDefaultProxy(null != newDefaultNetwork
                 ? newDefaultNetwork.linkProperties.getHttpProxy() : null);
         resetHttpProxyForNonDefaultNetwork(oldDefaultNetwork);
@@ -10085,7 +10888,7 @@
     private void processListenRequests(@NonNull final NetworkAgentInfo nai) {
         // For consistency with previous behaviour, send onLost callbacks before onAvailable.
         processNewlyLostListenRequests(nai);
-        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+        notifyNetworkCallbacks(nai, CALLBACK_CAP_CHANGED);
         processNewlySatisfiedListenRequests(nai);
     }
 
@@ -10098,7 +10901,7 @@
             if (!nr.isListen()) continue;
             if (nai.isSatisfyingRequest(nr.requestId) && !nai.satisfies(nr)) {
                 nai.removeRequest(nr.requestId);
-                callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_LOST, 0);
+                callCallbackForRequest(nri, nai, CALLBACK_LOST, 0);
             }
         }
     }
@@ -10430,7 +11233,7 @@
                 notifyNetworkAvailable(event.mNewNetwork, event.mNetworkRequestInfo);
             } else {
                 callCallbackForRequest(event.mNetworkRequestInfo, event.mOldNetwork,
-                        ConnectivityManager.CALLBACK_LOST, 0);
+                        CALLBACK_LOST, 0);
             }
         }
 
@@ -10474,7 +11277,7 @@
         if (null != localInfoChangedAgents) {
             for (final NetworkAgentInfo nai : localInfoChangedAgents) {
                 notifyNetworkCallbacks(nai,
-                        ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+                        CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
             }
         }
 
@@ -10517,7 +11320,7 @@
         if (Objects.equals(nai.networkCapabilities, newNc)) return;
         updateNetworkPermissions(nai, newNc);
         nai.getAndSetNetworkCapabilities(newNc);
-        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+        notifyNetworkCallbacks(nai, CALLBACK_CAP_CHANGED);
     }
 
     private void updateLegacyTypeTrackerAndVpnLockdownForRematch(
@@ -10886,7 +11689,7 @@
             rematchAllNetworksAndRequests();
 
             // This has to happen after matching the requests, because callbacks are just requests.
-            notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
+            notifyNetworkCallbacks(networkAgent, CALLBACK_PRECHECK);
         } else if (state == NetworkInfo.State.DISCONNECTED) {
             networkAgent.disconnect();
             if (networkAgent.isVPN()) {
@@ -10919,31 +11722,48 @@
     protected void notifyNetworkAvailable(NetworkAgentInfo nai, NetworkRequestInfo nri) {
         mHandler.removeMessages(EVENT_TIMEOUT_NETWORK_REQUEST, nri);
         if (nri.mPendingIntent != null) {
-            sendPendingIntentForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE);
+            sendPendingIntentForRequest(nri, nai, CALLBACK_AVAILABLE);
             // Attempt no subsequent state pushes where intents are involved.
             return;
         }
 
-        final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE);
-        final boolean metered = nai.networkCapabilities.isMetered();
-        final boolean vpnBlocked = isUidBlockedByVpn(nri.mAsUid, mVpnBlockedUidRanges);
-        callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE,
-                getBlockedState(blockedReasons, metered, vpnBlocked));
+        callCallbackForRequest(nri, nai, CALLBACK_AVAILABLE, getBlockedState(nai, nri.mAsUid));
     }
 
     // Notify the requests on this NAI that the network is now lingered.
     private void notifyNetworkLosing(@NonNull final NetworkAgentInfo nai, final long now) {
         final int lingerTime = (int) (nai.getInactivityExpiry() - now);
-        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
+        notifyNetworkCallbacks(nai, CALLBACK_LOSING, lingerTime);
     }
 
-    private static int getBlockedState(int reasons, boolean metered, boolean vpnBlocked) {
+    private int getPermissionBlockedState(final int uid, final int reasons) {
+        // Before V, the blocked reasons come from NPMS, and that code already behaves as if the
+        // change was disabled: apps without the internet permission will never be told they are
+        // blocked.
+        if (!mDeps.isAtLeastV()) return reasons;
+
+        if (hasInternetPermission(uid)) return reasons;
+
+        return mDeps.isChangeEnabled(NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, uid)
+                ? reasons | BLOCKED_REASON_NETWORK_RESTRICTED
+                : BLOCKED_REASON_NONE;
+    }
+
+    private int getBlockedState(int uid, int reasons, boolean metered, boolean vpnBlocked) {
+        reasons = getPermissionBlockedState(uid, reasons);
         if (!metered) reasons &= ~BLOCKED_METERED_REASON_MASK;
         return vpnBlocked
                 ? reasons | BLOCKED_REASON_LOCKDOWN_VPN
                 : reasons & ~BLOCKED_REASON_LOCKDOWN_VPN;
     }
 
+    private int getBlockedState(@NonNull NetworkAgentInfo nai, int uid) {
+        final boolean metered = nai.networkCapabilities.isMetered();
+        final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
+        final int blockedReasons = mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE);
+        return getBlockedState(uid, blockedReasons, metered, vpnBlocked);
+    }
+
     private void setUidBlockedReasons(int uid, @BlockedReason int blockedReasons) {
         if (blockedReasons == BLOCKED_REASON_NONE) {
             mUidBlockedReasons.delete(uid);
@@ -10978,10 +11798,12 @@
                     ? isUidBlockedByVpn(nri.mAsUid, newBlockedUidRanges)
                     : oldVpnBlocked;
 
-            final int oldBlockedState = getBlockedState(blockedReasons, oldMetered, oldVpnBlocked);
-            final int newBlockedState = getBlockedState(blockedReasons, newMetered, newVpnBlocked);
+            final int oldBlockedState = getBlockedState(
+                    nri.mAsUid, blockedReasons, oldMetered, oldVpnBlocked);
+            final int newBlockedState = getBlockedState(
+                    nri.mAsUid, blockedReasons, newMetered, newVpnBlocked);
             if (oldBlockedState != newBlockedState) {
-                callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
+                callCallbackForRequest(nri, nai, CALLBACK_BLK_CHANGED,
                         newBlockedState);
             }
         }
@@ -10998,8 +11820,9 @@
             final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
 
             final int oldBlockedState = getBlockedState(
-                    mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE), metered, vpnBlocked);
-            final int newBlockedState = getBlockedState(blockedReasons, metered, vpnBlocked);
+                    uid, mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE), metered, vpnBlocked);
+            final int newBlockedState =
+                    getBlockedState(uid, blockedReasons, metered, vpnBlocked);
             if (oldBlockedState == newBlockedState) {
                 continue;
             }
@@ -11007,7 +11830,7 @@
                 NetworkRequest nr = nai.requestAt(i);
                 NetworkRequestInfo nri = mNetworkRequests.get(nr);
                 if (nri != null && nri.mAsUid == uid) {
-                    callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
+                    callCallbackForRequest(nri, nai, CALLBACK_BLK_CHANGED,
                             newBlockedState);
                 }
             }
@@ -11365,6 +12188,10 @@
                         return 0;
                     }
                     case "get-package-networking-enabled": {
+                        if (!mDeps.isAtLeastT()) {
+                            throw new UnsupportedOperationException(
+                                    "This command is not supported on T-");
+                        }
                         final String packageName = getNextArg();
                         final int rule = getPackageFirewallRule(
                                 ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3, packageName);
@@ -11394,6 +12221,10 @@
                         return 0;
                     }
                     case "get-background-networking-enabled-for-uid": {
+                        if (!mDeps.isAtLeastT()) {
+                            throw new UnsupportedOperationException(
+                                    "This command is not supported on T-");
+                        }
                         final Integer uid = parseIntegerArgument(getNextArg());
                         if (null == uid) {
                             onHelp();
@@ -12023,7 +12854,7 @@
         // This NetworkCapabilities is only used for matching to Networks. Clear out its owner uid
         // and administrator uids to be safe.
         final NetworkCapabilities nc = new NetworkCapabilities(request.networkCapabilities);
-        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+        restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
 
         final NetworkRequest requestWithId =
                 new NetworkRequest(
@@ -12035,6 +12866,7 @@
         // handleRegisterConnectivityDiagnosticsCallback(). nri will be cleaned up as part of the
         // callback's binder death.
         final NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, requestWithId);
+        nri.mPerUidCounter.incrementCountOrThrow(nri.mUid);
         final ConnectivityDiagnosticsCallbackInfo cbInfo =
                 new ConnectivityDiagnosticsCallbackInfo(callback, nri, callingPackageName);
 
@@ -12630,8 +13462,8 @@
         if (um.isManagedProfile(profile.getIdentifier())) {
             return true;
         }
-        if (mDeps.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
-        return false;
+
+        return mDeps.isAtLeastT() && dpm.getDeviceOwnerComponentOnAnyUser() != null;
     }
 
     /**
@@ -12896,11 +13728,12 @@
         requests.add(createDefaultInternetRequestForTransport(
                 TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
 
-        // request: restricted Satellite internet
+        // request: Satellite internet, satellite network could be restricted or constrained
         final NetworkCapabilities cap = new NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
                 .addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE)
                 .build();
         requests.add(createNetworkRequest(NetworkRequest.Type.REQUEST, cap));
@@ -13129,7 +13962,20 @@
         final ArraySet<NetworkRequestInfo> perAppCallbackRequestsToUpdate =
                 getPerAppCallbackRequestsToUpdate();
         final ArraySet<NetworkRequestInfo> nrisToRegister = new ArraySet<>(nris);
-        handleRemoveNetworkRequests(perAppCallbackRequestsToUpdate);
+        // This method does not need to modify perUidCounter and mBlockedStatusTrackingUids because:
+        // - |nris| only contains per-app network requests created by ConnectivityService which
+        //    are internal requests and have no messenger and are not associated with any callbacks,
+        //    and so do not need to be tracked in perUidCounter and mBlockedStatusTrackingUids.
+        // - The requests in perAppCallbackRequestsToUpdate are removed, modified, and re-added,
+        //   but the same number of requests is removed and re-added, and none of the requests
+        //   changes mUid and mAsUid, so the perUidCounter and mBlockedStatusTrackingUids before
+        //   and after this method remains the same. Re-adding the requests does not modify
+        //   perUidCounter and mBlockedStatusTrackingUids (that is done when the app registers the
+        //   request), so removing them must not modify perUidCounter and mBlockedStatusTrackingUids
+        //   either.
+        // TODO(b/341228979): Modify nris in place instead of removing them and re-adding them
+        handleRemoveNetworkRequests(perAppCallbackRequestsToUpdate,
+                false /* untrackUids */);
         nrisToRegister.addAll(
                 createPerAppCallbackRequestsToRegister(perAppCallbackRequestsToUpdate));
         handleRegisterNetworkRequests(nrisToRegister);
@@ -13364,40 +14210,17 @@
             throw new IllegalStateException(e);
         }
 
-        try {
-            mBpfNetMaps.setDataSaverEnabled(enable);
-        } catch (ServiceSpecificException | UnsupportedOperationException e) {
-            Log.e(TAG, "Failed to set data saver " + enable + " : " + e);
-        }
-    }
-
-    @Override
-    public void updateMeteredNetworkAllowList(final int uid, final boolean add) {
-        enforceNetworkStackOrSettingsPermission();
-
-        try {
-            if (add) {
-                mBpfNetMaps.addNiceApp(uid);
-            } else {
-                mBpfNetMaps.removeNiceApp(uid);
+        synchronized (mBlockedStatusTrackingUids) {
+            try {
+                mBpfNetMaps.setDataSaverEnabled(enable);
+            } catch (ServiceSpecificException | UnsupportedOperationException e) {
+                Log.e(TAG, "Failed to set data saver " + enable + " : " + e);
+                return;
             }
-        } catch (ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
 
-    @Override
-    public void updateMeteredNetworkDenyList(final int uid, final boolean add) {
-        enforceNetworkStackOrSettingsPermission();
-
-        try {
-            if (add) {
-                mBpfNetMaps.addNaughtyApp(uid);
-            } else {
-                mBpfNetMaps.removeNaughtyApp(uid);
+            if (shouldTrackUidsForBlockedStatusCallbacks()) {
+                updateTrackingUidsBlockedReasons();
             }
-        } catch (ServiceSpecificException e) {
-            throw new IllegalStateException(e);
         }
     }
 
@@ -13420,6 +14243,12 @@
     public void setUidFirewallRule(final int chain, final int uid, final int rule) {
         enforceNetworkStackOrSettingsPermission();
 
+        if (chain == FIREWALL_CHAIN_BACKGROUND && !mBackgroundFirewallChainEnabled) {
+            Log.i(TAG, "Ignoring operation setUidFirewallRule on the background chain because the"
+                    + " feature is disabled.");
+            return;
+        }
+
         // There are only two type of firewall rule: FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
         int firewallRule = getFirewallRuleType(chain, rule);
 
@@ -13427,13 +14256,24 @@
             throw new IllegalArgumentException("setUidFirewallRule with invalid rule: " + rule);
         }
 
-        try {
-            mBpfNetMaps.setUidRule(chain, uid, firewallRule);
-        } catch (ServiceSpecificException e) {
-            throw new IllegalStateException(e);
+        synchronized (mBlockedStatusTrackingUids) {
+            try {
+                mBpfNetMaps.setUidRule(chain, uid, firewallRule);
+            } catch (ServiceSpecificException e) {
+                throw new IllegalStateException(e);
+            }
+            if (shouldTrackUidsForBlockedStatusCallbacks()
+                    && mBlockedStatusTrackingUids.get(uid, 0) != 0) {
+                mHandler.sendMessage(mHandler.obtainMessage(EVENT_BLOCKED_REASONS_CHANGED,
+                        List.of(new Pair<>(uid, mBpfNetMaps.getUidNetworkingBlockedReasons(uid)))));
+            }
+            if (shouldTrackFirewallDestroySocketReasons()) {
+                maybePostFirewallDestroySocketReasons(chain, Set.of(uid));
+            }
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private int getPackageFirewallRule(final int chain, final String packageName)
             throws PackageManager.NameNotFoundException {
         final PackageManager pm = mContext.getPackageManager();
@@ -13441,6 +14281,7 @@
         return getUidFirewallRule(chain, appId);
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     @Override
     public int getUidFirewallRule(final int chain, final int uid) {
         enforceNetworkStackOrSettingsPermission();
@@ -13454,6 +14295,8 @@
             case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1:
             case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2:
             case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3:
+            case ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER:
+            case ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN:
                 defaultRule = FIREWALL_RULE_ALLOW;
                 break;
             case ConnectivityManager.FIREWALL_CHAIN_DOZABLE:
@@ -13461,6 +14304,7 @@
             case ConnectivityManager.FIREWALL_CHAIN_RESTRICTED:
             case ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY:
             case ConnectivityManager.FIREWALL_CHAIN_BACKGROUND:
+            case ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW:
                 defaultRule = FIREWALL_RULE_DENY;
                 break;
             default:
@@ -13471,31 +14315,71 @@
         return rule;
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private Set<Integer> getUidsOnFirewallChain(final int chain) throws ErrnoException {
+        if (BpfNetMapsUtils.isFirewallAllowList(chain)) {
+            return mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(chain);
+        } else {
+            return mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(chain);
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private void closeSocketsForFirewallChainLocked(final int chain)
             throws ErrnoException, SocketException, InterruptedIOException {
+        final Set<Integer> uidsOnChain = getUidsOnFirewallChain(chain);
         if (BpfNetMapsUtils.isFirewallAllowList(chain)) {
             // Allowlist means the firewall denies all by default, uids must be explicitly allowed
             // So, close all non-system socket owned by uids that are not explicitly allowed
             Set<Range<Integer>> ranges = new ArraySet<>();
             ranges.add(new Range<>(Process.FIRST_APPLICATION_UID, Integer.MAX_VALUE));
-            final Set<Integer> exemptUids = mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(chain);
-            mDeps.destroyLiveTcpSockets(ranges, exemptUids);
+            mDeps.destroyLiveTcpSockets(ranges, uidsOnChain /* exemptUids */);
         } else {
             // Denylist means the firewall allows all by default, uids must be explicitly denied
             // So, close socket owned by uids that are explicitly denied
-            final Set<Integer> ownerUids = mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(chain);
-            mDeps.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+            mDeps.destroyLiveTcpSocketsByOwnerUids(uidsOnChain /* ownerUids */);
         }
     }
 
+    private void maybePostClearFirewallDestroySocketReasons(int chain) {
+        if (chain != FIREWALL_CHAIN_BACKGROUND) {
+            // TODO (b/300681644): Support other firewall chains
+            return;
+        }
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_CLEAR_FIREWALL_DESTROY_SOCKET_REASONS,
+                DESTROY_SOCKET_REASON_FIREWALL_BACKGROUND, 0 /* arg2 */));
+    }
+
     @Override
     public void setFirewallChainEnabled(final int chain, final boolean enable) {
         enforceNetworkStackOrSettingsPermission();
 
-        try {
-            mBpfNetMaps.setChildChain(chain, enable);
-        } catch (ServiceSpecificException e) {
-            throw new IllegalStateException(e);
+        if (chain == FIREWALL_CHAIN_BACKGROUND && !mBackgroundFirewallChainEnabled) {
+            Log.i(TAG, "Ignoring operation setFirewallChainEnabled on the background chain because"
+                    + " the feature is disabled.");
+            return;
+        }
+        if (METERED_ALLOW_CHAINS.contains(chain) || METERED_DENY_CHAINS.contains(chain)) {
+            // Metered chains are used from a separate bpf program that is triggered by iptables
+            // and can not be controlled by setFirewallChainEnabled.
+            throw new UnsupportedOperationException(
+                    "Chain (" + chain + ") can not be controlled by setFirewallChainEnabled");
+        }
+
+        synchronized (mBlockedStatusTrackingUids) {
+            try {
+                mBpfNetMaps.setChildChain(chain, enable);
+            } catch (ServiceSpecificException e) {
+                throw new IllegalStateException(e);
+            }
+            if (shouldTrackUidsForBlockedStatusCallbacks()) {
+                updateTrackingUidsBlockedReasons();
+            }
+            if (shouldTrackFirewallDestroySocketReasons() && !enable) {
+                // Clear destroy socket reasons so that CS does not destroy sockets of apps that
+                // have network access.
+                maybePostClearFirewallDestroySocketReasons(chain);
+            }
         }
 
         if (mDeps.isAtLeastU() && enable) {
@@ -13507,10 +14391,58 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @GuardedBy("mBlockedStatusTrackingUids")
+    private void updateTrackingUidsBlockedReasons() {
+        if (mBlockedStatusTrackingUids.size() == 0) {
+            return;
+        }
+        final ArrayList<Pair<Integer, Integer>> uidBlockedReasonsList = new ArrayList<>();
+        for (int i = 0; i < mBlockedStatusTrackingUids.size(); i++) {
+            final int uid = mBlockedStatusTrackingUids.keyAt(i);
+            uidBlockedReasonsList.add(
+                    new Pair<>(uid, mBpfNetMaps.getUidNetworkingBlockedReasons(uid)));
+        }
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_BLOCKED_REASONS_CHANGED,
+                uidBlockedReasonsList));
+    }
+
+    private int getFirewallDestroySocketReasons(final int blockedReasons) {
+        int destroySocketReasons = DESTROY_SOCKET_REASON_NONE;
+        if ((blockedReasons & BLOCKED_REASON_APP_BACKGROUND) != BLOCKED_REASON_NONE) {
+            destroySocketReasons |= DESTROY_SOCKET_REASON_FIREWALL_BACKGROUND;
+        }
+        return destroySocketReasons;
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @GuardedBy("mBlockedStatusTrackingUids")
+    private void maybePostFirewallDestroySocketReasons(int chain, Set<Integer> uids) {
+        if (chain != FIREWALL_CHAIN_BACKGROUND) {
+            // TODO (b/300681644): Support other firewall chains
+            return;
+        }
+        final ArrayList<Pair<Integer, Integer>> reasonsList = new ArrayList<>();
+        for (int uid: uids) {
+            final int blockedReasons = mBpfNetMaps.getUidNetworkingBlockedReasons(uid);
+            final int destroySocketReaons = getFirewallDestroySocketReasons(blockedReasons);
+            reasonsList.add(new Pair<>(uid, destroySocketReaons));
+        }
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_UPDATE_FIREWALL_DESTROY_SOCKET_REASONS,
+                reasonsList));
+    }
+
     @Override
     public boolean getFirewallChainEnabled(final int chain) {
         enforceNetworkStackOrSettingsPermission();
 
+        if (METERED_ALLOW_CHAINS.contains(chain) || METERED_DENY_CHAINS.contains(chain)) {
+            // Metered chains are used from a separate bpf program that is triggered by iptables
+            // and can not be controlled by setFirewallChainEnabled.
+            throw new UnsupportedOperationException(
+                    "getFirewallChainEnabled can not return status of chain (" + chain + ")");
+        }
+
         return mBpfNetMaps.isChainEnabled(chain);
     }
 
@@ -13518,7 +14450,37 @@
     public void replaceFirewallChain(final int chain, final int[] uids) {
         enforceNetworkStackOrSettingsPermission();
 
-        mBpfNetMaps.replaceUidChain(chain, uids);
+        if (chain == FIREWALL_CHAIN_BACKGROUND && !mBackgroundFirewallChainEnabled) {
+            Log.i(TAG, "Ignoring operation replaceFirewallChain on the background chain because"
+                    + " the feature is disabled.");
+            return;
+        }
+
+        synchronized (mBlockedStatusTrackingUids) {
+            // replaceFirewallChain removes uids that are currently on the chain and put |uids| on
+            // the chain.
+            // So this method could change blocked reasons of uids that are currently on chain +
+            // |uids|.
+            final Set<Integer> affectedUids = new ArraySet<>();
+            if (shouldTrackFirewallDestroySocketReasons()) {
+                try {
+                    affectedUids.addAll(getUidsOnFirewallChain(chain));
+                } catch (ErrnoException e) {
+                    Log.e(TAG, "Failed to get uids on chain(" + chain + "): " + e);
+                }
+                for (final int uid: uids) {
+                    affectedUids.add(uid);
+                }
+            }
+
+            mBpfNetMaps.replaceUidChain(chain, uids);
+            if (shouldTrackUidsForBlockedStatusCallbacks()) {
+                updateTrackingUidsBlockedReasons();
+            }
+            if (shouldTrackFirewallDestroySocketReasons()) {
+                maybePostFirewallDestroySocketReasons(chain, affectedUids);
+            }
+        }
     }
 
     @Override
@@ -13532,4 +14494,16 @@
         enforceNetworkStackPermission(mContext);
         return mRoutingCoordinatorService;
     }
+
+    @Override
+    public long getEnabledConnectivityManagerFeatures() {
+        long features = 0;
+        // The bitmask must be built based on final properties initialized in the constructor, to
+        // ensure that it does not change over time and is always consistent between
+        // ConnectivityManager and ConnectivityService.
+        if (mUseDeclaredMethodsForCallbacksEnabled) {
+            features |= ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS;
+        }
+        return features;
+    }
 }
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 843b7b3..4d39d7d 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -267,6 +267,7 @@
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
         nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
         nc.setNetworkSpecifier(new TestNetworkSpecifier(iface));
         nc.setAdministratorUids(administratorUids);
         if (!isMetered) {
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index 04d0fc1..f5fa4fb 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -91,42 +91,62 @@
             @NonNull final TelephonyManager t,
             @NonNull final TelephonyManagerShim telephonyManagerShim,
             final boolean requestRestrictedWifiEnabled,
-            @NonNull BiConsumer<Integer, Integer> listener) {
+            @NonNull BiConsumer<Integer, Integer> listener,
+            @NonNull final Handler connectivityServiceHandler) {
         mContext = c;
         mTelephonyManager = t;
         mTelephonyManagerShim = telephonyManagerShim;
-        final HandlerThread thread = deps.makeHandlerThread();
-        thread.start();
-        mHandler = new Handler(thread.getLooper());
         mUseCallbacksForServiceChanged = deps.isFeatureEnabled(
                 c, CARRIER_SERVICE_CHANGED_USE_CALLBACK);
         mRequestRestrictedWifiEnabled = requestRestrictedWifiEnabled;
         mListener = listener;
+        if (mRequestRestrictedWifiEnabled) {
+            mHandler = connectivityServiceHandler;
+        } else {
+            final HandlerThread thread = deps.makeHandlerThread();
+            thread.start();
+            mHandler = new Handler(thread.getLooper());
+            synchronized (mLock) {
+                registerSimConfigChangedReceiver();
+                simConfigChanged();
+            }
+        }
+    }
+
+    private void registerSimConfigChangedReceiver() {
         final IntentFilter filter = new IntentFilter();
         filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
-        synchronized (mLock) {
-            // Never unregistered because the system server never stops
-            c.registerReceiver(new BroadcastReceiver() {
-                @Override
-                public void onReceive(final Context context, final Intent intent) {
-                    switch (intent.getAction()) {
-                        case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
-                            simConfigChanged();
-                            break;
-                        default:
-                            Log.d(TAG, "Unknown intent received, action: " + intent.getAction());
-                    }
+        // Never unregistered because the system server never stops
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(final Context context, final Intent intent) {
+                switch (intent.getAction()) {
+                    case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
+                        simConfigChanged();
+                        break;
+                    default:
+                        Log.d(TAG, "Unknown intent received, action: " + intent.getAction());
                 }
-            }, filter, null, mHandler);
-            simConfigChanged();
+            }
+        }, filter, null, mHandler);
+    }
+
+    /**
+     * Start CarrierPrivilegeAuthenticator
+     */
+    public void start() {
+        if (mRequestRestrictedWifiEnabled) {
+            registerSimConfigChangedReceiver();
+            mHandler.post(this::simConfigChanged);
         }
     }
 
     public CarrierPrivilegeAuthenticator(@NonNull final Context c,
             @NonNull final TelephonyManager t, final boolean requestRestrictedWifiEnabled,
-            @NonNull BiConsumer<Integer, Integer> listener) {
+            @NonNull BiConsumer<Integer, Integer> listener,
+            @NonNull final Handler connectivityServiceHandler) {
         this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t),
-                requestRestrictedWifiEnabled, listener);
+                requestRestrictedWifiEnabled, listener, connectivityServiceHandler);
     }
 
     public static class Dependencies {
@@ -146,6 +166,10 @@
     }
 
     private void simConfigChanged() {
+        //  If mRequestRestrictedWifiEnabled is false, constructor calls simConfigChanged
+        if (mRequestRestrictedWifiEnabled) {
+            ensureRunningOnHandlerThread();
+        }
         synchronized (mLock) {
             unregisterCarrierPrivilegesListeners();
             mModemCount = mTelephonyManager.getActiveModemCount();
@@ -188,6 +212,7 @@
         public void onCarrierPrivilegesChanged(
                 @NonNull List<String> privilegedPackageNames,
                 @NonNull int[] privilegedUids) {
+            ensureRunningOnHandlerThread();
             if (mUseCallbacksForServiceChanged) return;
             // Re-trigger the synchronous check (which is also very cheap due
             // to caching in CarrierPrivilegesTracker). This allows consistency
@@ -198,6 +223,7 @@
         @Override
         public void onCarrierServiceChanged(@Nullable final String carrierServicePackageName,
                 final int carrierServiceUid) {
+            ensureRunningOnHandlerThread();
             if (!mUseCallbacksForServiceChanged) {
                 // Re-trigger the synchronous check (which is also very cheap due
                 // to caching in CarrierPrivilegesTracker). This allows consistency
@@ -439,6 +465,13 @@
         }
     }
 
+    private void ensureRunningOnHandlerThread() {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on handler thread: " + Thread.currentThread().getName());
+        }
+    }
+
     public void dump(IndentingPrintWriter pw) {
         pw.println("CarrierPrivilegeAuthenticator:");
         pw.println("mRequestRestrictedWifiEnabled = " + mRequestRestrictedWifiEnabled);
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index daaf91d..e808746 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -41,6 +41,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.BpfMap;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.InterfaceParams;
@@ -255,6 +256,7 @@
         @Nullable
         public IBpfMap<ClatIngress6Key, ClatIngress6Value> getBpfIngress6Map() {
             try {
+                // written from clatd.c
                 return new BpfMap<>(CLAT_INGRESS6_MAP_PATH,
                        ClatIngress6Key.class, ClatIngress6Value.class);
             } catch (ErrnoException e) {
@@ -267,6 +269,7 @@
         @Nullable
         public IBpfMap<ClatEgress4Key, ClatEgress4Value> getBpfEgress4Map() {
             try {
+                // written from clatd.c
                 return new BpfMap<>(CLAT_EGRESS4_MAP_PATH,
                        ClatEgress4Key.class, ClatEgress4Value.class);
             } catch (ErrnoException e) {
@@ -279,6 +282,7 @@
         @Nullable
         public IBpfMap<CookieTagMapKey, CookieTagMapValue> getBpfCookieTagMap() {
             try {
+                // also read and written from other locations
                 return new BpfMap<>(COOKIE_TAG_MAP_PATH,
                        CookieTagMapKey.class, CookieTagMapValue.class);
             } catch (ErrnoException e) {
@@ -399,6 +403,10 @@
         mCookieTagMap = mDeps.getBpfCookieTagMap();
     }
 
+    // Note that this may only be called on a brand new v4-* interface,
+    // because it uses bpfmap.insertEntry() which fails if entry exists,
+    // and because the value includes (initialized to 0) byte/packet
+    // counters, so a replace (instead of insert) would wipe those stats.
     private void maybeStartBpf(final ClatdTracker tracker) {
         if (mIngressMap == null || mEgressMap == null) return;
 
@@ -520,31 +528,6 @@
         }
     }
 
-    private void maybeCleanUp(ParcelFileDescriptor tunFd, ParcelFileDescriptor readSock6,
-            ParcelFileDescriptor writeSock6) {
-        if (tunFd != null) {
-            try {
-                tunFd.close();
-            } catch (IOException e) {
-                Log.e(TAG, "Fail to close tun file descriptor " + e);
-            }
-        }
-        if (readSock6 != null) {
-            try {
-                readSock6.close();
-            } catch (IOException e) {
-                Log.e(TAG, "Fail to close read socket " + e);
-            }
-        }
-        if (writeSock6 != null) {
-            try {
-                writeSock6.close();
-            } catch (IOException e) {
-                Log.e(TAG, "Fail to close write socket " + e);
-            }
-        }
-    }
-
     private void tagSocketAsClat(long cookie) throws IOException {
         if (mCookieTagMap == null) {
             throw new IOException("Cookie tag map is not initialized");
@@ -600,42 +583,6 @@
             throw new IOException("Prefix must be 96 bits long: " + nat64Prefix);
         }
 
-        // [1] Pick an IPv4 address from 192.0.0.4, 192.0.0.5, 192.0.0.6 ..
-        final String v4Str;
-        try {
-            v4Str = mDeps.selectIpv4Address(INIT_V4ADDR_STRING, INIT_V4ADDR_PREFIX_LEN);
-        } catch (IOException e) {
-            throw new IOException("no IPv4 addresses were available for clat: " + e);
-        }
-
-        final Inet4Address v4;
-        try {
-            v4 = (Inet4Address) InetAddresses.parseNumericAddress(v4Str);
-        } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
-            throw new IOException("Invalid IPv4 address " + v4Str);
-        }
-
-        // [2] Generate a checksum-neutral IID.
-        final Integer fwmark = getFwmark(netId);
-        final String pfx96Str = nat64Prefix.getAddress().getHostAddress();
-        final String v6Str;
-        try {
-            v6Str = mDeps.generateIpv6Address(iface, v4Str, pfx96Str, fwmark);
-        } catch (IOException e) {
-            throw new IOException("no IPv6 addresses were available for clat: " + e);
-        }
-
-        final Inet6Address pfx96 = (Inet6Address) nat64Prefix.getAddress();
-        final Inet6Address v6;
-        try {
-            v6 = (Inet6Address) InetAddresses.parseNumericAddress(v6Str);
-        } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
-            throw new IOException("Invalid IPv6 address " + v6Str);
-        }
-
-        // [3] Open, configure and bring up the tun interface.
-        // Create the v4-... tun interface.
-
         // Initialize all required file descriptors with null pointer. This makes the following
         // error handling easier. Simply always call #maybeCleanUp for closing file descriptors,
         // if any valid ones, in error handling.
@@ -643,145 +590,123 @@
         ParcelFileDescriptor readSock6 = null;
         ParcelFileDescriptor writeSock6 = null;
 
-        final String tunIface = CLAT_PREFIX + iface;
-        try {
-            tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface));
-        } catch (IOException e) {
-            throw new IOException("Create tun interface " + tunIface + " failed: " + e);
-        }
+        long cookie = 0;
+        boolean isSocketTagged = false;
 
-        final int tunIfIndex = mDeps.getInterfaceIndex(tunIface);
-        if (tunIfIndex == INVALID_IFINDEX) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("Fail to get interface index for interface " + tunIface);
-        }
+        try {
+            // [1] Pick an IPv4 address from 192.0.0.4, 192.0.0.5, 192.0.0.6 ..
+            final String v4Str = mDeps.selectIpv4Address(INIT_V4ADDR_STRING,
+                    INIT_V4ADDR_PREFIX_LEN);
+            final Inet4Address v4 = (Inet4Address) InetAddresses.parseNumericAddress(v4Str);
 
-        // disable IPv6 on it - failing to do so is not a critical error
-        try {
-            mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
-        } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
-        }
+            // [2] Generate a checksum-neutral IID.
+            final Integer fwmark = getFwmark(netId);
+            final String pfx96Str = nat64Prefix.getAddress().getHostAddress();
+            final String v6Str = mDeps.generateIpv6Address(iface, v4Str, pfx96Str, fwmark);
+            final Inet6Address pfx96 = (Inet6Address) nat64Prefix.getAddress();
+            final Inet6Address v6 = (Inet6Address) InetAddresses.parseNumericAddress(v6Str);
 
-        // Detect ipv4 mtu.
-        final int detectedMtu;
-        try {
-            detectedMtu = mDeps.detectMtu(pfx96Str,
-                ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
-        } catch (IOException e) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("Detect MTU on " + tunIface + " failed: " + e);
-        }
-        final int mtu = adjustMtu(detectedMtu);
-        Log.i(TAG, "detected ipv4 mtu of " + detectedMtu + " adjusted to " + mtu);
+            // [3] Open and configure local 464xlat read/write sockets.
+            // Opens a packet socket to receive IPv6 packets in clatd.
 
-        // Config tun interface mtu, address and bring up.
-        try {
-            mNetd.interfaceSetMtu(tunIface, mtu);
-        } catch (RemoteException | ServiceSpecificException e) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
-        }
-        final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
-        ifConfig.ifName = tunIface;
-        ifConfig.ipv4Addr = v4Str;
-        ifConfig.prefixLength = 32;
-        ifConfig.hwAddr = "";
-        ifConfig.flags = new String[] {IF_STATE_UP};
-        try {
-            mNetd.interfaceSetCfg(ifConfig);
-        } catch (RemoteException | ServiceSpecificException e) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
-                    + ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
-        }
-
-        // [4] Open and configure local 464xlat read/write sockets.
-        // Opens a packet socket to receive IPv6 packets in clatd.
-        try {
             // Use a JNI call to get native file descriptor instead of Os.socket() because we would
             // like to use ParcelFileDescriptor to manage file descriptor. But ctor
             // ParcelFileDescriptor(FileDescriptor fd) is a @hide function. Need to use native file
             // descriptor to initialize ParcelFileDescriptor object instead.
             readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
-        } catch (IOException e) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("Open packet socket failed: " + e);
-        }
 
-        // Opens a raw socket with a given fwmark to send IPv6 packets in clatd.
-        try {
+            // Opens a raw socket with a given fwmark to send IPv6 packets in clatd.
             // Use a JNI call to get native file descriptor instead of Os.socket(). See above
             // reason why we use jniOpenPacketSocket6().
             writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
-        } catch (IOException e) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("Open raw socket failed: " + e);
-        }
 
-        final int ifIndex = mDeps.getInterfaceIndex(iface);
-        if (ifIndex == INVALID_IFINDEX) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("Fail to get interface index for interface " + iface);
-        }
+            final int ifIndex = mDeps.getInterfaceIndex(iface);
+            if (ifIndex == INVALID_IFINDEX) {
+                throw new IOException("Fail to get interface index for interface " + iface);
+            }
 
-        // Start translating packets to the new prefix.
-        try {
+            // Start translating packets to the new prefix.
             mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6Str, ifIndex);
-        } catch (IOException e) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("add anycast sockopt failed: " + e);
-        }
-
-        // Tag socket as AID_CLAT to avoid duplicated CLAT data usage accounting.
-        final long cookie;
-        try {
+            // Tag socket as AID_CLAT to avoid duplicated CLAT data usage accounting.
             cookie = mDeps.getSocketCookie(writeSock6.getFileDescriptor());
             tagSocketAsClat(cookie);
-        } catch (IOException e) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("tag raw socket failed: " + e);
-        }
-
-        // Update our packet socket filter to reflect the new 464xlat IP address.
-        try {
+            isSocketTagged = true;
+            // Update our packet socket filter to reflect the new 464xlat IP address.
             mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6Str, ifIndex);
-        } catch (IOException e) {
-            try {
-                untagSocket(cookie);
-            } catch (IOException e2) {
-                Log.e(TAG, "untagSocket cookie " + cookie + " failed: " + e2);
-            }
-            maybeCleanUp(tunFd, readSock6, writeSock6);
-            throw new IOException("configure packet socket failed: " + e);
-        }
 
-        // [5] Start clatd.
-        final int pid;
-        try {
-            pid = mDeps.startClatd(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
-                    writeSock6.getFileDescriptor(), iface, pfx96Str, v4Str, v6Str);
-        } catch (IOException e) {
-            try {
-                untagSocket(cookie);
-            } catch (IOException e2) {
-                Log.e(TAG, "untagSocket cookie " + cookie + " failed: " + e2);
+            // [4] Open, configure and bring up the tun interface.
+            // Create the v4-... tun interface.
+            final String tunIface = CLAT_PREFIX + iface;
+            tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface));
+            final int tunIfIndex = mDeps.getInterfaceIndex(tunIface);
+            if (tunIfIndex == INVALID_IFINDEX) {
+                throw new IOException("Fail to get interface index for interface " + tunIface);
             }
-            throw new IOException("Error start clatd on " + iface + ": " + e);
-        } finally {
+            // disable IPv6 on it - failing to do so is not a critical error
+            mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
+            // Detect ipv4 mtu.
+            final int detectedMtu = mDeps.detectMtu(pfx96Str,
+                    ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
+            final int mtu = adjustMtu(detectedMtu);
+            Log.i(TAG, "detected ipv4 mtu of " + detectedMtu + " adjusted to " + mtu);
+            // Config tun interface mtu, address and bring up.
+            mNetd.interfaceSetMtu(tunIface, mtu);
+            final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
+            ifConfig.ifName = tunIface;
+            ifConfig.ipv4Addr = v4Str;
+            ifConfig.prefixLength = 32;
+            ifConfig.hwAddr = "";
+            ifConfig.flags = new String[] {IF_STATE_UP};
+            mNetd.interfaceSetCfg(ifConfig);
+
+            // [5] Start clatd.
+            final int pid = mDeps.startClatd(tunFd.getFileDescriptor(),
+                    readSock6.getFileDescriptor(), writeSock6.getFileDescriptor(), iface, pfx96Str,
+                    v4Str, v6Str);
             // The file descriptors have been duplicated (dup2) to clatd in native_startClatd().
-            // Close these file descriptor stubs which are unused anymore.
-            maybeCleanUp(tunFd, readSock6, writeSock6);
+            // Close these file descriptor stubs in finally block.
+
+            // [6] Initialize and store clatd tracker object.
+            mClatdTracker = new ClatdTracker(iface, ifIndex, tunIface, tunIfIndex, v4, v6, pfx96,
+                    pid, cookie);
+
+            // [7] Start BPF
+            maybeStartBpf(mClatdTracker);
+
+            return v6Str;
+        } catch (IOException | RemoteException | ServiceSpecificException | ClassCastException
+                 | IllegalArgumentException | NullPointerException e) {
+            if (isSocketTagged) {
+                try {
+                    untagSocket(cookie);
+                } catch (IOException e2) {
+                    Log.e(TAG, "untagSocket cookie " + cookie + " failed: " + e2);
+                }
+            }
+            throw new IOException("Failed to start clat ", e);
+        } finally {
+            if (tunFd != null) {
+                try {
+                    tunFd.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Fail to close tun file descriptor " + e);
+                }
+            }
+            if (readSock6 != null) {
+                try {
+                    readSock6.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Fail to close read socket " + e);
+                }
+            }
+            if (writeSock6 != null) {
+                try {
+                    writeSock6.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Fail to close write socket " + e);
+                }
+            }
         }
-
-        // [6] Initialize and store clatd tracker object.
-        mClatdTracker = new ClatdTracker(iface, ifIndex, tunIface, tunIfIndex, v4, v6, pfx96,
-                pid, cookie);
-
-        // [7] Start BPF
-        maybeStartBpf(mClatdTracker);
-
-        return v6Str;
     }
 
     private void maybeStopBpf(final ClatdTracker tracker) {
@@ -847,12 +772,12 @@
             if (mIngressMap.isEmpty()) {
                 pw.println("<empty>");
             }
-            pw.println("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif");
+            pw.println("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif (packets bytes)");
             pw.increaseIndent();
             mIngressMap.forEach((k, v) -> {
                 // TODO: print interface name
-                pw.println(String.format("%d %s/96 %s -> %s %d", k.iif, k.pfx96, k.local6,
-                        v.local4, v.oif));
+                pw.println(String.format("%d %s/96 %s -> %s %d (%d %d)", k.iif, k.pfx96, k.local6,
+                        v.local4, v.oif, v.packets, v.bytes));
             });
             pw.decreaseIndent();
         } catch (ErrnoException e) {
@@ -870,12 +795,13 @@
             if (mEgressMap.isEmpty()) {
                 pw.println("<empty>");
             }
-            pw.println("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif");
+            pw.println("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif (packets bytes)");
             pw.increaseIndent();
             mEgressMap.forEach((k, v) -> {
                 // TODO: print interface name
-                pw.println(String.format("%d %s -> %s %s/96 %d %s", k.iif, k.local4, v.local6,
-                        v.pfx96, v.oif, v.oifIsEthernet != 0 ? "ether" : "rawip"));
+                pw.println(String.format("%d %s -> %s %s/96 %d %s (%d %d)", k.iif, k.local4,
+                        v.local6, v.pfx96, v.oif, v.oifIsEthernet != 0 ? "ether" : "rawip",
+                        v.packets, v.bytes));
             });
             pw.decreaseIndent();
         } catch (ErrnoException e) {
@@ -884,6 +810,29 @@
     }
 
     /**
+     * Dump raw BPF map into base64 encoded strings {@literal "<base64 key>,<base64 value>"}.
+     * Allow to dump only one map in each call. For test only.
+     *
+     * @param pw print writer.
+     * @param isEgress4Map whether to dump the egress4 map (true) or the ingress6 map (false).
+     *
+     * Usage:
+     * $ dumpsys connectivity {clatEgress4RawBpfMap|clatIngress6RawBpfMap}
+     *
+     * Output:
+     * {@literal <base64 encoded key #1>,<base64 encoded value #1>}
+     * {@literal <base64 encoded key #2>,<base64 encoded value #2>}
+     * ..
+     */
+    public void dumpRawMap(@NonNull IndentingPrintWriter pw, boolean isEgress4Map) {
+        if (isEgress4Map) {
+            BpfDump.dumpRawMap(mEgressMap, pw);
+        } else {
+            BpfDump.dumpRawMap(mIngressMap, pw);
+        }
+    }
+
+    /**
      * Dump the coordinator information.
      *
      * @param pw print writer.
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index bf09160..df87316 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -38,6 +38,20 @@
 
     public static final String REQUEST_RESTRICTED_WIFI =
             "request_restricted_wifi";
+
+    public static final String INGRESS_TO_VPN_ADDRESS_FILTERING =
+            "ingress_to_vpn_address_filtering";
+
+    public static final String BACKGROUND_FIREWALL_CHAIN = "background_firewall_chain";
+
+    public static final String DELAY_DESTROY_SOCKETS = "delay_destroy_sockets";
+
+    public static final String USE_DECLARED_METHODS_FOR_CALLBACKS =
+            "use_declared_methods_for_callbacks";
+
+    public static final String QUEUE_CALLBACKS_FOR_FROZEN_APPS =
+            "queue_callbacks_for_frozen_apps";
+
     private boolean mNoRematchAllRequestsOnRegister;
 
     /**
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index cf6127f..917ad4d 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -23,6 +23,7 @@
 import android.os.Binder;
 import android.os.Process;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.util.Log;
 
@@ -67,8 +68,8 @@
     }
 
     private void enforceBlockPortPermission() {
-        final int uid = Binder.getCallingUid();
-        if (uid == Process.ROOT_UID || uid == Process.PHONE_UID) return;
+        final int appId = UserHandle.getAppId(Binder.getCallingUid());
+        if (appId == Process.ROOT_UID || appId == Process.PHONE_UID) return;
         PermissionUtils.enforceNetworkStackPermission(mContext);
     }
 
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index 8e6854a..ac02229 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -390,6 +390,7 @@
                 : new String[0];            // Off
         paramsParcel.transportTypes = nc.getTransportTypes();
         paramsParcel.meteredNetwork = nc.isMetered();
+        paramsParcel.interfaceNames = lp.getAllInterfaceNames().toArray(new String[0]);
         // Prepare to track the validation status of the DNS servers in the
         // resolver config when private DNS is in opportunistic or strict mode.
         if (useTls) {
@@ -403,13 +404,14 @@
         }
 
         Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
-                + "%d, %d, %s, %s, %s, %b)", paramsParcel.netId,
+                + "%d, %d, %s, %s, %s, %b, %s)", paramsParcel.netId,
                 Arrays.toString(paramsParcel.servers), Arrays.toString(paramsParcel.domains),
                 paramsParcel.sampleValiditySeconds, paramsParcel.successThreshold,
                 paramsParcel.minSamples, paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
                 paramsParcel.retryCount, paramsParcel.tlsName,
                 Arrays.toString(paramsParcel.tlsServers),
-                Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork));
+                Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork,
+                Arrays.toString(paramsParcel.interfaceNames)));
 
         try {
             mDnsResolver.setResolverConfiguration(paramsParcel);
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 15d6adb..9c2b9e8 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -64,6 +64,8 @@
         return "/sys/fs/bpf/net_shared/map_" + which + "_map";
     }
 
+    private final boolean mHaveProgram = TcUtils.isBpfProgramUsable(PROG_PATH);
+
     private Set<String> mAttachedIfaces;
 
     private final BpfMap<Struct.S32, DscpPolicyValue> mBpfDscpIpv4Policies;
@@ -325,6 +327,7 @@
      * Attach BPF program
      */
     private boolean attachProgram(@NonNull String iface) {
+        if (!mHaveProgram) return false;
         try {
             NetworkInterface netIface = NetworkInterface.getByName(iface);
             TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL,
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 48af9fa..21dbb45 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -29,6 +29,7 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.net.TransportInfo;
 import android.net.wifi.WifiInfo;
+import android.os.Build;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.telephony.SubscriptionInfo;
@@ -39,6 +40,8 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 
+import androidx.annotation.RequiresApi;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.metrics.DailykeepaliveInfoReported;
 import com.android.metrics.DurationForNumOfKeepalive;
@@ -279,6 +282,7 @@
          *
          * @param dailyKeepaliveInfoReported the proto to write to statsD.
          */
+        @RequiresApi(Build.VERSION_CODES.TIRAMISU)
         public void writeStats(DailykeepaliveInfoReported dailyKeepaliveInfoReported) {
             ConnectivityStatsLog.write(
                     ConnectivityStatsLog.DAILY_KEEPALIVE_INFO_REPORTED,
diff --git a/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
index 4d5001b..af4aee5 100644
--- a/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
+++ b/service/src/com/android/server/connectivity/MulticastRoutingCoordinatorService.java
@@ -27,6 +27,8 @@
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
 
+import static com.android.net.module.util.CollectionUtils.getIndexForValue;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.MulticastRoutingConfig;
@@ -150,7 +152,7 @@
     }
 
     private Integer getInterfaceIndex(String ifName) {
-        int mapIndex = mInterfaces.indexOfValue(ifName);
+        int mapIndex = getIndexForValue(mInterfaces, ifName);
         if (mapIndex < 0) return null;
         return mInterfaces.keyAt(mapIndex);
     }
@@ -166,14 +168,18 @@
     public void applyMulticastRoutingConfig(
             final String iifName, final String oifName, final MulticastRoutingConfig newConfig) {
         checkOnHandlerThread();
+        Objects.requireNonNull(iifName, "IifName can't be null");
+        Objects.requireNonNull(oifName, "OifName can't be null");
 
         if (newConfig.getForwardingMode() != FORWARD_NONE) {
             // Make sure iif and oif are added as multicast forwarding interfaces
-            try {
-                maybeAddAndTrackInterface(iifName);
-                maybeAddAndTrackInterface(oifName);
-            } catch (IllegalStateException e) {
-                Log.e(TAG, "Failed to apply multicast routing config, ", e);
+            if (!maybeAddAndTrackInterface(iifName) || !maybeAddAndTrackInterface(oifName)) {
+                Log.e(
+                        TAG,
+                        "Failed to apply multicast routing config from "
+                                + iifName
+                                + " to "
+                                + oifName);
                 return;
             }
         }
@@ -246,7 +252,7 @@
         if (virtualIndex == null) return;
 
         updateMfcs();
-        mInterfaces.removeAt(mInterfaces.indexOfValue(ifName));
+        mInterfaces.removeAt(getIndexForValue(mInterfaces, ifName));
         mVirtualInterfaces.remove(virtualIndex);
         try {
             mDependencies.setsockoptMrt6DelMif(mMulticastRoutingFd, virtualIndex);
@@ -256,9 +262,14 @@
         }
     }
 
+    /**
+     * Returns the next available virtual index for multicast routing, or -1 if the number of
+     * virtual interfaces has reached max value.
+     */
     private int getNextAvailableVirtualIndex() {
         if (mVirtualInterfaces.size() >= MAX_NUM_OF_MULTICAST_VIRTUAL_INTERFACES) {
-            throw new IllegalStateException("Can't allocate new multicast virtual interface");
+            Log.e(TAG, "Can't allocate new multicast virtual interface");
+            return -1;
         }
         for (int i = 0; i < mVirtualInterfaces.size(); i++) {
             if (!mVirtualInterfaces.contains(i)) {
@@ -270,7 +281,7 @@
 
     @VisibleForTesting
     public Integer getVirtualInterfaceIndex(String ifName) {
-        int mapIndex = mVirtualInterfaces.indexOfValue(ifName);
+        int mapIndex = getIndexForValue(mVirtualInterfaces, ifName);
         if (mapIndex < 0) return null;
         return mVirtualInterfaces.keyAt(mapIndex);
     }
@@ -289,12 +300,23 @@
         return mVirtualInterfaces.get(virtualIndex);
     }
 
-    private void maybeAddAndTrackInterface(String ifName) {
+    /**
+     * Returns {@code true} if the interfaces is added and tracked, or {@code false} when failed
+     * to add the interface.
+     */
+    private boolean maybeAddAndTrackInterface(String ifName) {
         checkOnHandlerThread();
-        if (mVirtualInterfaces.indexOfValue(ifName) >= 0) return;
+        if (getIndexForValue(mVirtualInterfaces, ifName) >= 0) return true;
 
         int nextVirtualIndex = getNextAvailableVirtualIndex();
+        if (nextVirtualIndex < 0) {
+            return false;
+        }
         int ifIndex = mDependencies.getInterfaceIndex(ifName);
+        if (ifIndex == 0) {
+            Log.e(TAG, "Can't get interface index for " + ifName);
+            return false;
+        }
         final StructMif6ctl mif6ctl =
                     new StructMif6ctl(
                             nextVirtualIndex,
@@ -307,10 +329,11 @@
             Log.d(TAG, "Added mifi " + nextVirtualIndex + " to MIF");
         } catch (ErrnoException e) {
             Log.e(TAG, "failed to add multicast virtual interface", e);
-            return;
+            return false;
         }
         mVirtualInterfaces.put(nextVirtualIndex, ifName);
         mInterfaces.put(ifIndex, ifName);
+        return true;
     }
 
     @VisibleForTesting
@@ -796,13 +819,12 @@
             NetworkUtils.setsockoptBytes(fd, IPPROTO_IPV6, MRT6_DEL_MFC, bytes);
         }
 
-        public Integer getInterfaceIndex(String ifName) {
-            try {
-                NetworkInterface ni = NetworkInterface.getByName(ifName);
-                return ni.getIndex();
-            } catch (NullPointerException | SocketException e) {
-                return null;
-            }
+        /**
+         * Returns the interface index for an interface name, or 0 if the interface index could
+         * not be found.
+         */
+        public int getInterfaceIndex(String ifName) {
+            return Os.if_nametoindex(ifName);
         }
 
         public NetworkInterface getNetworkInterface(int physicalIndex) {
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index 065922d..a979681 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -202,13 +202,13 @@
             try {
                 addrStr = mClatCoordinator.clatStart(baseIface, getNetId(), mNat64PrefixInUse);
             } catch (IOException e) {
-                Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+                Log.e(TAG, "Error starting clatd on " + baseIface, e);
             }
         } else {
             try {
                 addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
             } catch (RemoteException | ServiceSpecificException e) {
-                Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+                Log.e(TAG, "Error starting clatd on " + baseIface, e);
             }
         }
         mIface = CLAT_PREFIX + baseIface;
@@ -217,7 +217,7 @@
         try {
             mIPv6Address = (Inet6Address) InetAddresses.parseNumericAddress(addrStr);
         } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
-            Log.e(TAG, "Invalid IPv6 address " + addrStr);
+            Log.e(TAG, "Invalid IPv6 address " + addrStr , e);
         }
         if (mPrefixDiscoveryRunning && !isPrefixDiscoveryNeeded()) {
             stopPrefixDiscovery();
@@ -622,6 +622,18 @@
         }
     }
 
+    /**
+     * Dump the raw BPF maps in 464XLAT
+     *
+     * @param pw print writer.
+     * @param isEgress4Map whether to dump the egress4 map (true) or the ingress6 map (false).
+     */
+    public void dumpRawBpfMap(IndentingPrintWriter pw, boolean isEgress4Map) {
+        if (SdkLevel.isAtLeastT()) {
+            mClatCoordinator.dumpRawMap(pw, isEgress4Map);
+        }
+    }
+
     @Override
     public String toString() {
         return "mBaseIface: " + mBaseIface + ", mIface: " + mIface + ", mState: " + mState;
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index 3db37e5..8a2e72c 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -567,7 +567,9 @@
 
         @Override
         public void close() {
-            IoUtils.closeQuietly(mFileDescriptor);
+            if (mFileDescriptor != null) {
+                IoUtils.closeQuietly(mFileDescriptor);
+            }
         }
     }
 
@@ -611,6 +613,7 @@
                 setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
             } catch (ErrnoException | IOException e) {
                 mMeasurement.recordFailure(e.toString());
+                close();
                 return;
             }
             mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
@@ -695,6 +698,7 @@
                         NetworkConstants.DNS_SERVER_PORT);
             } catch (ErrnoException | IOException e) {
                 mMeasurement.recordFailure(e.toString());
+                close();
                 return;
             }
 
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index 7707122..fd41ee6 100644
--- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -170,9 +170,11 @@
                     && !TextUtils.isEmpty(nai.linkProperties.getCaptivePortalData()
                     .getVenueFriendlyName())) {
                 name = nai.linkProperties.getCaptivePortalData().getVenueFriendlyName();
+            } else if (!TextUtils.isEmpty(extraInfo)) {
+                name = extraInfo;
             } else {
-                name = TextUtils.isEmpty(extraInfo)
-                        ? WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid()) : extraInfo;
+                final String ssid = WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid());
+                name = ssid == null ? "" : ssid;
             }
             // Only notify for Internet-capable networks.
             if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
diff --git a/service/src/com/android/server/connectivity/SatelliteAccessController.java b/service/src/com/android/server/connectivity/SatelliteAccessController.java
index b53abce..2cdc932 100644
--- a/service/src/com/android/server/connectivity/SatelliteAccessController.java
+++ b/service/src/com/android/server/connectivity/SatelliteAccessController.java
@@ -20,7 +20,10 @@
 import android.annotation.NonNull;
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.os.Handler;
@@ -49,7 +52,6 @@
     private final Context mContext;
     private final Dependencies mDeps;
     private final DefaultMessageRoleListener mDefaultMessageRoleListener;
-    private final UserManager mUserManager;
     private final Consumer<Set<Integer>> mCallback;
     private final Handler mConnectivityServiceHandler;
 
@@ -114,7 +116,6 @@
             @NonNull final Handler connectivityServiceInternalHandler) {
         mContext = c;
         mDeps = deps;
-        mUserManager = mContext.getSystemService(UserManager.class);
         mDefaultMessageRoleListener = new DefaultMessageRoleListener();
         mCallback = callback;
         mConnectivityServiceHandler = connectivityServiceInternalHandler;
@@ -165,9 +166,6 @@
     }
 
     // on Role sms change triggered by OnRoleHoldersChangedListener()
-    // TODO(b/326373613): using UserLifecycleListener, callback to be received when user removed for
-    // user delete scenario. This to be used to update uid list and ML Layer request can also be
-    // updated.
     private void onRoleSmsChanged(@NonNull UserHandle userHandle) {
         int userId = userHandle.getIdentifier();
         if (userId == Process.INVALID_UID) {
@@ -184,9 +182,8 @@
                 mAllUsersSatelliteNetworkFallbackUidCache.get(userId, new ArraySet<>());
 
         Log.i(TAG, "currentUser : role_sms_packages: " + userId + " : " + packageNames);
-        final Set<Integer> newUidsForUser = !packageNames.isEmpty()
-                ? updateSatelliteNetworkFallbackUidListCache(packageNames, userHandle)
-                : new ArraySet<>();
+        final Set<Integer> newUidsForUser =
+                updateSatelliteNetworkFallbackUidListCache(packageNames, userHandle);
         Log.i(TAG, "satellite_fallback_uid: " + newUidsForUser);
 
         // on Role change, update the multilayer request at ConnectivityService with updated
@@ -197,6 +194,11 @@
 
         mAllUsersSatelliteNetworkFallbackUidCache.put(userId, newUidsForUser);
 
+        // Update all users fallback cache for user, send cs fallback to update ML request
+        reportSatelliteNetworkFallbackUids();
+    }
+
+    private void reportSatelliteNetworkFallbackUids() {
         // Merge all uids of multiple users available
         Set<Integer> mergedSatelliteNetworkFallbackUidCache = new ArraySet<>();
         for (int i = 0; i < mAllUsersSatelliteNetworkFallbackUidCache.size(); i++) {
@@ -210,27 +212,48 @@
         mCallback.accept(mergedSatelliteNetworkFallbackUidCache);
     }
 
-    private List<String> getRoleSmsChangedPackageName(UserHandle userHandle) {
-        try {
-            return mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
-        } catch (RuntimeException e) {
-            Log.wtf(TAG, "Could not get package name at role sms change update due to: " + e);
-            return null;
-        }
-    }
-
-    /** Register OnRoleHoldersChangedListener */
     public void start() {
         mConnectivityServiceHandler.post(this::updateAllUserRoleSmsUids);
+
+        // register sms OnRoleHoldersChangedListener
         mDefaultMessageRoleListener.register();
+
+        // Monitor for User removal intent, to update satellite fallback uids.
+        IntentFilter userRemovedFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final String action = intent.getAction();
+                if (Intent.ACTION_USER_REMOVED.equals(action)) {
+                    final UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
+                    if (userHandle == null) return;
+                    updateSatelliteFallbackUidListOnUserRemoval(userHandle.getIdentifier());
+                } else {
+                    Log.wtf(TAG, "received unexpected intent: " + action);
+                }
+            }
+        }, userRemovedFilter, null, mConnectivityServiceHandler);
+
     }
 
     private void updateAllUserRoleSmsUids() {
-        List<UserHandle> existingUsers = mUserManager.getUserHandles(true /* excludeDying */);
+        UserManager userManager = mContext.getSystemService(UserManager.class);
+        // get existing user handles of available users
+        List<UserHandle> existingUsers = userManager.getUserHandles(true /*excludeDying*/);
+
         // Iterate through the user handles and obtain their uids with role sms and satellite
         // communication permission
+        Log.i(TAG, "existing users: " + existingUsers);
         for (UserHandle userHandle : existingUsers) {
             onRoleSmsChanged(userHandle);
         }
     }
+
+    private void updateSatelliteFallbackUidListOnUserRemoval(int userIdRemoved) {
+        Log.i(TAG, "user id removed:" + userIdRemoved);
+        if (mAllUsersSatelliteNetworkFallbackUidCache.contains(userIdRemoved)) {
+            mAllUsersSatelliteNetworkFallbackUidCache.remove(userIdRemoved);
+            reportSatelliteNetworkFallbackUids();
+        }
+    }
 }
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index f7b42a6..ed0670d 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -39,12 +39,13 @@
         "device/com/android/net/module/util/DeviceConfigUtils.java",
         "device/com/android/net/module/util/DomainUtils.java",
         "device/com/android/net/module/util/FdEventsReader.java",
+        "device/com/android/net/module/util/FeatureVersions.java",
+        "device/com/android/net/module/util/HandlerUtils.java",
         "device/com/android/net/module/util/NetworkMonitorUtils.java",
         "device/com/android/net/module/util/PacketReader.java",
         "device/com/android/net/module/util/SharedLog.java",
         "device/com/android/net/module/util/SocketUtils.java",
-        "device/com/android/net/module/util/FeatureVersions.java",
-        "device/com/android/net/module/util/HandlerUtils.java",
+        "device/com/android/net/module/util/SyncStateMachine.java",
         // This library is used by system modules, for which the system health impact of Kotlin
         // has not yet been evaluated. Annotations may need jarjar'ing.
         // "src_devicecommon/**/*.kt",
@@ -57,17 +58,13 @@
         "//apex_available:platform",
     ],
     visibility: [
-        "//frameworks/base/packages/Tethering",
         "//packages/modules/Connectivity:__subpackages__",
         "//packages/modules/Connectivity/framework:__subpackages__",
-        "//frameworks/opt/net/ike",
-        "//frameworks/opt/net/wifi/service",
-        "//packages/modules/Wifi/service",
-        "//frameworks/opt/net/telephony",
         "//packages/modules/NetworkStack:__subpackages__",
         "//packages/modules/CaptivePortalLogin",
     ],
     static_libs: [
+        "modules-utils-statemachine",
         "net-utils-framework-common",
     ],
     libs: [
@@ -124,6 +121,8 @@
     ],
 }
 
+// The net-utils-device-common-bpf library requires the callers to contain
+// net-utils-device-common-struct-base.
 java_library {
     name: "net-utils-device-common-bpf",
     srcs: [
@@ -133,9 +132,8 @@
         "device/com/android/net/module/util/BpfUtils.java",
         "device/com/android/net/module/util/IBpfMap.java",
         "device/com/android/net/module/util/JniUtil.java",
-        "device/com/android/net/module/util/Struct.java",
+        "device/com/android/net/module/util/SingleWriterBpfMap.java",
         "device/com/android/net/module/util/TcUtils.java",
-        "framework/com/android/net/module/util/HexDump.java",
     ],
     sdk_version: "module_current",
     min_sdk_version: "30",
@@ -146,6 +144,7 @@
     libs: [
         "androidx.annotation_annotation",
         "framework-connectivity.stubs.module_lib",
+        "net-utils-device-common-struct-base",
     ],
     apex_available: [
         "com.android.tethering",
@@ -158,12 +157,9 @@
 }
 
 java_library {
-    name: "net-utils-device-common-struct",
+    name: "net-utils-device-common-struct-base",
     srcs: [
-        "device/com/android/net/module/util/Ipv6Utils.java",
-        "device/com/android/net/module/util/PacketBuilder.java",
         "device/com/android/net/module/util/Struct.java",
-        "device/com/android/net/module/util/structs/*.java",
     ],
     sdk_version: "module_current",
     min_sdk_version: "30",
@@ -176,6 +172,7 @@
     ],
     libs: [
         "androidx.annotation_annotation",
+        "framework-annotations-lib", // Required by InetAddressUtils.java
         "framework-connectivity.stubs.module_lib",
     ],
     apex_available: [
@@ -188,26 +185,30 @@
     },
 }
 
-// The net-utils-multicast-forwarding-structs library requires the callers to
-// contain net-utils-device-common-bpf.
+// The net-utils-device-common-struct library requires the callers to contain
+// net-utils-device-common-struct-base.
 java_library {
-    name: "net-utils-multicast-forwarding-structs",
+    name: "net-utils-device-common-struct",
     srcs: [
-        "device/com/android/net/module/util/structs/StructMf6cctl.java",
-        "device/com/android/net/module/util/structs/StructMif6ctl.java",
-        "device/com/android/net/module/util/structs/StructMrt6Msg.java",
+        "device/com/android/net/module/util/Ipv6Utils.java",
+        "device/com/android/net/module/util/PacketBuilder.java",
+        "device/com/android/net/module/util/structs/*.java",
     ],
     sdk_version: "module_current",
     min_sdk_version: "30",
     visibility: [
         "//packages/modules/Connectivity:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
     ],
     libs: [
-        // Only Struct.java is needed from "net-utils-device-common-bpf"
-        "net-utils-device-common-bpf",
+        "androidx.annotation_annotation",
+        "framework-annotations-lib", // Required by IpUtils.java
+        "framework-connectivity.stubs.module_lib",
+        "net-utils-device-common-struct-base",
     ],
     apex_available: [
         "com.android.tethering",
+        "//apex_available:platform",
     ],
     lint: {
         strict_updatability_linting: true,
@@ -216,7 +217,7 @@
 }
 
 // The net-utils-device-common-netlink library requires the callers to contain
-// net-utils-device-common-struct.
+// net-utils-device-common-struct and net-utils-device-common-struct-base.
 java_library {
     name: "net-utils-device-common-netlink",
     srcs: [
@@ -235,6 +236,7 @@
         // statically link here because callers of this library might already have a static
         // version linked.
         "net-utils-device-common-struct",
+        "net-utils-device-common-struct-base",
     ],
     apex_available: [
         "com.android.tethering",
@@ -247,7 +249,7 @@
 }
 
 // The net-utils-device-common-ip library requires the callers to contain
-// net-utils-device-common-struct.
+// net-utils-device-common-struct and net-utils-device-common-struct-base.
 java_library {
     // TODO : this target should probably be folded into net-utils-device-common
     name: "net-utils-device-common-ip",
@@ -275,7 +277,7 @@
         "//apex_available:platform",
     ],
     lint: {
-        baseline_filename: "lint-baseline.xml",
+        strict_updatability_linting: true,
         error_checks: ["NewApi"],
     },
 }
@@ -338,12 +340,10 @@
     ],
     sdk_version: "module_current",
     min_sdk_version: "30",
-    static_libs: [
-        "modules-utils-build_system",
-    ],
     libs: [
         "framework-annotations-lib",
         "framework-connectivity",
+        "modules-utils-build_system",
     ],
     // TODO: remove "apex_available:platform".
     apex_available: [
@@ -438,6 +438,7 @@
         "device/com/android/net/module/util/SharedLog.java",
         "framework/com/android/net/module/util/ByteUtils.java",
         "framework/com/android/net/module/util/CollectionUtils.java",
+        "framework/com/android/net/module/util/DnsUtils.java",
         "framework/com/android/net/module/util/HexDump.java",
         "framework/com/android/net/module/util/LinkPropertiesUtils.java",
     ],
@@ -449,6 +450,135 @@
     visibility: ["//packages/modules/Connectivity/service-t"],
 }
 
+// net-utils-framework-connectivity is only for framework-connectivity.
+java_library {
+    name: "net-utils-framework-connectivity",
+    srcs: [
+        ":net-utils-framework-connectivity-srcs",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity/framework",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib",
+        "framework-configinfrastructure",
+        "framework-connectivity.stubs.module_lib",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+java_defaults {
+    name: "net-utils-non-bootclasspath-defaults",
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    libs: [
+        "androidx.annotation_annotation",
+        "framework-annotations-lib",
+        "framework-configinfrastructure",
+        "framework-connectivity",
+        "framework-connectivity.stubs.module_lib",
+        "framework-connectivity-t.stubs.module_lib",
+        "framework-location.stubs.module_lib",
+        "framework-tethering",
+        "unsupportedappusage",
+    ],
+    static_libs: [
+        "modules-utils-build_system",
+        "modules-utils-statemachine",
+        "net-utils-non-bootclasspath-aidl-java",
+        "netd-client",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    defaults_visibility: [
+        "//visibility:private",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
+// net-utils-service-connectivity is only for service-connectivity.
+java_library {
+    name: "net-utils-service-connectivity",
+    srcs: [
+        ":net-utils-all-srcs",
+    ],
+    exclude_srcs: [
+        ":net-utils-framework-connectivity-srcs",
+    ],
+    libs: [
+        "net-utils-framework-connectivity",
+    ],
+    defaults: ["net-utils-non-bootclasspath-defaults"],
+    jarjar_rules: "jarjar-rules-shared.txt",
+    visibility: [
+        "//packages/modules/Connectivity/service",
+        "//packages/modules/Connectivity/staticlibs/tests/unit",
+    ],
+}
+
+java_library {
+    name: "net-utils-connectivity-apks",
+    srcs: [
+        ":net-utils-all-srcs",
+        ":framework-connectivity-shared-srcs",
+    ],
+    defaults: ["net-utils-non-bootclasspath-defaults"],
+    jarjar_rules: "jarjar-rules-shared.txt",
+    visibility: [
+        "//packages/modules/CaptivePortalLogin:__subpackages__",
+        "//packages/modules/Connectivity/Tethering",
+    ],
+}
+
+aidl_interface {
+    name: "net-utils-non-bootclasspath-aidl",
+    srcs: [
+        ":net-utils-aidl-srcs",
+    ],
+    unstable: true,
+    backend: {
+        java: {
+            enabled: true,
+            min_sdk_version: "30",
+            apex_available: [
+                "com.android.tethering",
+                "com.android.wifi",
+                "//apex_available:platform",
+            ],
+        },
+        cpp: {
+            enabled: false,
+        },
+        ndk: {
+            enabled: false,
+        },
+        rust: {
+            enabled: false,
+        },
+    },
+    include_dirs: [
+        "packages/modules/Connectivity/framework/aidl-export",
+    ],
+    visibility: [
+        "//system/tools/aidl/build",
+    ],
+}
+
 // Use a filegroup and not a library for telephony sources, as framework-annotations cannot be
 // included either (some annotations would be duplicated on the bootclasspath).
 filegroup {
@@ -466,8 +596,7 @@
     ],
 }
 
-// Use a filegroup and not a library for wifi sources, as this needs corresponding jar-jar
-// rules on the wifi side.
+// Filegroup to build lib used by Wifi framework
 // Any class here *must* have a corresponding jarjar rule in the wifi build rules.
 filegroup {
     name: "net-utils-framework-wifi-common-srcs",
@@ -479,24 +608,78 @@
         "framework/com/android/net/module/util/NetUtils.java",
     ],
     path: "framework",
+    visibility: ["//visibility:private"],
+}
+
+// Use a file group containing classes necessary for framework-connectivity. The file group should
+// be as small as possible because because the classes end up in the bootclasspath and R8 is not
+// used to remove unused classes.
+filegroup {
+    name: "net-utils-framework-connectivity-srcs",
+    srcs: [
+        "device/com/android/net/module/util/BpfBitmap.java",
+        "device/com/android/net/module/util/BpfDump.java",
+        "device/com/android/net/module/util/BpfMap.java",
+        "device/com/android/net/module/util/BpfUtils.java",
+        "device/com/android/net/module/util/IBpfMap.java",
+        "device/com/android/net/module/util/JniUtil.java",
+        "device/com/android/net/module/util/SingleWriterBpfMap.java",
+        "device/com/android/net/module/util/Struct.java",
+        "device/com/android/net/module/util/TcUtils.java",
+        "framework/com/android/net/module/util/HexDump.java",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+filegroup {
+    name: "net-utils-all-srcs",
+    srcs: [
+        "device/**/*.java",
+        ":net-utils-framework-common-srcs",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+filegroup {
+    name: "net-utils-aidl-srcs",
+    srcs: [
+        "device/**/*.aidl",
+    ],
+    path: "device",
+    visibility: ["//visibility:private"],
+}
+
+java_library {
+    name: "net-utils-service-wifi",
+    srcs: [
+        ":net-utils-all-srcs",
+    ],
+    exclude_srcs: [":net-utils-framework-wifi-common-srcs"],
+    libs: [
+        "net-utils-framework-wifi",
+    ],
+    defaults: ["net-utils-non-bootclasspath-defaults"],
+
     visibility: [
-        "//frameworks/base",
+        "//packages/modules/Wifi/service",
+    ],
+    apex_available: [
+        "com.android.wifi",
     ],
 }
 
-// Use a filegroup and not a library for wifi sources, as this needs corresponding jar-jar
-// rules on the wifi side.
-// Any class here *must* have a corresponding jarjar rule in the wifi build rules.
-filegroup {
-    name: "net-utils-wifi-service-common-srcs",
-    srcs: [
-        "device/android/net/NetworkFactory.java",
-        "device/android/net/NetworkFactoryImpl.java",
-        "device/android/net/NetworkFactoryLegacyImpl.java",
-        "device/android/net/NetworkFactoryShim.java",
+java_library {
+    name: "net-utils-framework-wifi",
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    srcs: [":net-utils-framework-wifi-common-srcs"],
+    libs: [
+        "framework-annotations-lib",
+        "framework-connectivity.stubs.module_lib",
+        "unsupportedappusage",
     ],
     visibility: [
-        "//frameworks/opt/net/wifi/service",
-        "//packages/modules/Wifi/service",
+        "//packages/modules/Wifi/framework",
     ],
+    apex_available: ["com.android.wifi"],
 }
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
index 7549e71..4227194 100644
--- a/staticlibs/device/com/android/net/module/util/BpfDump.java
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -81,6 +81,26 @@
     }
 
     /**
+     * Dump the BpfMap entries with base64 format encoding.
+     */
+    public static <K extends Struct, V extends Struct> void dumpRawMap(IBpfMap<K, V> map,
+            PrintWriter pw) {
+        try {
+            if (map == null) {
+                pw.println("BPF map is null");
+                return;
+            }
+            if (map.isEmpty()) {
+                pw.println("No entries");
+                return;
+            }
+            map.forEach((k, v) -> pw.println(toBase64EncodedString(k, v)));
+        } catch (ErrnoException e) {
+            pw.println("Map dump end with error: " + Os.strerror(e.errno));
+        }
+    }
+
+    /**
      * Dump the BpfMap name and entries
      */
     public static <K extends Struct, V extends Struct> void dumpMap(IBpfMap<K, V> map,
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index da77ae8..0fbc25d 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -15,6 +15,7 @@
  */
 package com.android.net.module.util;
 
+import static android.system.OsConstants.EBUSY;
 import static android.system.OsConstants.EEXIST;
 import static android.system.OsConstants.ENOENT;
 
@@ -52,6 +53,9 @@
     public static final int BPF_F_RDONLY = 1 << 3;
     public static final int BPF_F_WRONLY = 1 << 4;
 
+    // magic value for jni consumption, invalid from kernel point of view
+    public static final int BPF_F_RDWR_EXCLUSIVE = BPF_F_RDONLY | BPF_F_WRONLY;
+
     public static final int BPF_MAP_TYPE_HASH = 1;
 
     private static final int BPF_F_NO_PREALLOC = 1;
@@ -69,6 +73,12 @@
     private static ConcurrentHashMap<Pair<String, Integer>, ParcelFileDescriptor> sFdCache =
             new ConcurrentHashMap<>();
 
+    private static ParcelFileDescriptor checkModeExclusivity(ParcelFileDescriptor fd, int mode)
+            throws ErrnoException {
+        if (mode == BPF_F_RDWR_EXCLUSIVE) throw new ErrnoException("cachedBpfFdGet", EBUSY);
+        return fd;
+    }
+
     private static ParcelFileDescriptor cachedBpfFdGet(String path, int mode,
                                                        int keySize, int valueSize)
             throws ErrnoException, NullPointerException {
@@ -79,12 +89,12 @@
         var key = Pair.create(path, (mode << 26) ^ (keySize << 16) ^ valueSize);
         // unlocked fetch is safe: map is concurrent read capable, and only inserted into
         ParcelFileDescriptor fd = sFdCache.get(key);
-        if (fd != null) return fd;
+        if (fd != null) return checkModeExclusivity(fd, mode);
         // ok, no cached fd present, need to grab a lock
         synchronized (BpfMap.class) {
             // need to redo the check
             fd = sFdCache.get(key);
-            if (fd != null) return fd;
+            if (fd != null) return checkModeExclusivity(fd, mode);
             // okay, we really haven't opened this before...
             fd = ParcelFileDescriptor.adoptFd(nativeBpfFdGet(path, mode, keySize, valueSize));
             sFdCache.put(key, fd);
diff --git a/staticlibs/device/com/android/net/module/util/BpfUtils.java b/staticlibs/device/com/android/net/module/util/BpfUtils.java
index cdd6fd7..a41eeba 100644
--- a/staticlibs/device/com/android/net/module/util/BpfUtils.java
+++ b/staticlibs/device/com/android/net/module/util/BpfUtils.java
@@ -39,6 +39,15 @@
     public static final int BPF_CGROUP_INET_SOCK_CREATE = 2;
     public static final int BPF_CGROUP_INET4_BIND = 8;
     public static final int BPF_CGROUP_INET6_BIND = 9;
+    public static final int BPF_CGROUP_INET4_CONNECT = 10;
+    public static final int BPF_CGROUP_INET6_CONNECT = 11;
+    public static final int BPF_CGROUP_UDP4_SENDMSG = 14;
+    public static final int BPF_CGROUP_UDP6_SENDMSG = 15;
+    public static final int BPF_CGROUP_UDP4_RECVMSG = 19;
+    public static final int BPF_CGROUP_UDP6_RECVMSG = 20;
+    public static final int BPF_CGROUP_GETSOCKOPT = 21;
+    public static final int BPF_CGROUP_SETSOCKOPT = 22;
+    public static final int BPF_CGROUP_INET_SOCK_RELEASE = 34;
 
     // Note: This is only guaranteed to be accurate on U+ devices. It is likely to be accurate
     // on T+ devices as well, but this is not guaranteed.
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 5b7cbb8..0426ace 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -17,6 +17,7 @@
 package com.android.net.module.util;
 
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.provider.DeviceConfig.NAMESPACE_CAPTIVEPORTALLOGIN;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
@@ -203,6 +204,29 @@
                 () -> getTetheringModuleVersion(context));
     }
 
+    /**
+     * Check whether or not one specific experimental feature for a particular namespace from
+     * {@link DeviceConfig} is enabled by comparing module package version
+     * with current version of property. If this property version is valid, the corresponding
+     * experimental feature would be enabled, otherwise disabled.
+     *
+     * This is useful to ensure that if a module install is rolled back, flags are not left fully
+     * rolled out on a version where they have not been well tested.
+     *
+     * If the feature is disabled by default and enabled by flag push, this method should be used.
+     * If the feature is enabled by default and disabled by flag push (kill switch),
+     * {@link #isCaptivePortalLoginFeatureNotChickenedOut(Context, String)} should be used.
+     *
+     * @param context The global context information about an app environment.
+     * @param name The name of the property to look up.
+     * @return true if this feature is enabled, or false if disabled.
+     */
+    public static boolean isCaptivePortalLoginFeatureEnabled(@NonNull Context context,
+            @NonNull String name) {
+        return isFeatureEnabled(NAMESPACE_CAPTIVEPORTALLOGIN, name, false /* defaultEnabled */,
+                () -> getPackageVersion(context));
+    }
+
     private static boolean isFeatureEnabled(@NonNull String namespace,
             String name, boolean defaultEnabled, Supplier<Long> packageVersionSupplier) {
         final int flagValue = getDeviceConfigPropertyInt(namespace, name, 0 /* default value */);
diff --git a/staticlibs/device/com/android/net/module/util/GrowingIntArray.java b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
new file mode 100644
index 0000000..d47738b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/GrowingIntArray.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.StringJoiner;
+import java.util.function.IntConsumer;
+import java.util.function.IntPredicate;
+
+/**
+ * A growing array of primitive ints.
+ *
+ * <p>This is similar to ArrayList&lt;Integer&gt;, but avoids the cost of boxing (each Integer costs
+ * 16 bytes) and creation / garbage collection of individual Integer objects.
+ *
+ * <p>This class does not use any heuristic for growing capacity, so every call to
+ * {@link #add(int)} may reallocate the backing array. Callers should use
+ * {@link #ensureHasCapacity(int)} to minimize this behavior when they plan to add several values.
+ */
+public class GrowingIntArray {
+    private int[] mValues;
+    private int mLength;
+
+    /**
+     * Create an empty GrowingIntArray with the given capacity.
+     */
+    public GrowingIntArray(int initialCapacity) {
+        mValues = new int[initialCapacity];
+        mLength = 0;
+    }
+
+    /**
+     * Create a GrowingIntArray with an initial array of values.
+     *
+     * <p>The array will be used as-is and may be modified, so callers must stop using it after
+     * calling this constructor.
+     */
+    protected GrowingIntArray(int[] initialValues) {
+        mValues = initialValues;
+        mLength = initialValues.length;
+    }
+
+    /**
+     * Add a value to the array.
+     */
+    public void add(int value) {
+        ensureHasCapacity(1);
+        mValues[mLength] = value;
+        mLength++;
+    }
+
+    /**
+     * Get the current number of values in the array.
+     */
+    public int length() {
+        return mLength;
+    }
+
+    /**
+     * Get the value at a given index.
+     *
+     * @throws ArrayIndexOutOfBoundsException if the index is out of bounds.
+     */
+    public int get(int index) {
+        if (index < 0 || index >= mLength) {
+            throw new ArrayIndexOutOfBoundsException(index);
+        }
+        return mValues[index];
+    }
+
+    /**
+     * Iterate over all values in the array.
+     */
+    public void forEach(@NonNull IntConsumer consumer) {
+        for (int i = 0; i < mLength; i++) {
+            consumer.accept(mValues[i]);
+        }
+    }
+
+    /**
+     * Remove all values matching a predicate.
+     *
+     * @return true if at least one value was removed.
+     */
+    public boolean removeValues(@NonNull IntPredicate predicate) {
+        int newQueueLength = 0;
+        for (int i = 0; i < mLength; i++) {
+            final int cb = mValues[i];
+            if (!predicate.test(cb)) {
+                mValues[newQueueLength] = cb;
+                newQueueLength++;
+            }
+        }
+        if (mLength != newQueueLength) {
+            mLength = newQueueLength;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether the array contains the given value.
+     */
+    public boolean contains(int value) {
+        for (int i = 0; i < mLength; i++) {
+            if (mValues[i] == value) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Remove all values from the array.
+     */
+    public void clear() {
+        mLength = 0;
+    }
+
+    /**
+     * Ensure at least the given number of values can be added to the array without reallocating.
+     *
+     * @param capacity The minimum number of additional values the array must be able to hold.
+     */
+    public void ensureHasCapacity(int capacity) {
+        if (mValues.length >= mLength + capacity) {
+            return;
+        }
+        mValues = Arrays.copyOf(mValues, mLength + capacity);
+    }
+
+    @VisibleForTesting
+    int getBackingArrayLength() {
+        return mValues.length;
+    }
+
+    /**
+     * Shrink the array backing this class to the minimum required length.
+     */
+    public void shrinkToLength() {
+        if (mValues.length != mLength) {
+            mValues = Arrays.copyOf(mValues, mLength);
+        }
+    }
+
+    /**
+     * Get values as array by shrinking the internal array to length and returning it.
+     *
+     * <p>This avoids reallocations if the array is already the correct length, but callers should
+     * stop using this instance of {@link GrowingIntArray} if they use the array returned by this
+     * method.
+     */
+    public int[] getMinimizedBackingArray() {
+        shrinkToLength();
+        return mValues;
+    }
+
+    /**
+     * Get the String representation of an item in the array, for use by {@link #toString()}.
+     */
+    protected String valueToString(int item) {
+        return String.valueOf(item);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        final StringJoiner joiner = new StringJoiner(",", "[", "]");
+        forEach(item -> joiner.add(valueToString(item)));
+        return joiner.toString();
+    }
+}
diff --git a/framework/src/android/net/IRoutingCoordinator.aidl b/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
similarity index 89%
rename from framework/src/android/net/IRoutingCoordinator.aidl
rename to staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
index cf02ec4..72a4a94 100644
--- a/framework/src/android/net/IRoutingCoordinator.aidl
+++ b/staticlibs/device/com/android/net/module/util/IRoutingCoordinator.aidl
@@ -14,11 +14,15 @@
  * limitations under the License.
  */
 
-package android.net;
+package com.android.net.module.util;
 
 import android.net.RouteInfo;
 
 /** @hide */
+// TODO: b/350630377 - This @Descriptor annotation workaround is to prevent the DESCRIPTOR from
+// being jarjared which changes the DESCRIPTOR and casues "java.lang.SecurityException: Binder
+// invocation to an incorrect interface" when calling the IPC.
+@Descriptor("value=no.jarjar.com.android.net.module.util.IRoutingCoordinator")
 interface IRoutingCoordinator {
    /**
     * Add a route for specific network
diff --git a/staticlibs/device/com/android/net/module/util/PacketBuilder.java b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
index 33e5bfa..a2dbd81 100644
--- a/staticlibs/device/com/android/net/module/util/PacketBuilder.java
+++ b/staticlibs/device/com/android/net/module/util/PacketBuilder.java
@@ -24,10 +24,13 @@
 import static com.android.net.module.util.IpUtils.ipChecksum;
 import static com.android.net.module.util.IpUtils.tcpChecksum;
 import static com.android.net.module.util.IpUtils.udpChecksum;
+import static com.android.net.module.util.NetworkStackConstants.IPPROTO_FRAGMENT;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_FRAGMENT_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_LEN_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.TCP_CHECKSUM_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.UDP_CHECKSUM_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.UDP_LENGTH_OFFSET;
@@ -37,6 +40,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.FragmentHeader;
 import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.TcpHeader;
@@ -47,6 +51,10 @@
 import java.net.Inet6Address;
 import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
 
 /**
  * The class is used to build a packet.
@@ -210,6 +218,20 @@
         }
     }
 
+    private void writeFragmentHeader(ByteBuffer buffer, short nextHeader, int offset,
+            boolean mFlag, int id) throws IOException {
+        if ((offset & 7) != 0) {
+            throw new IOException("Invalid offset value, must be multiple of 8");
+        }
+        final FragmentHeader fragmentHeader = new FragmentHeader(nextHeader,
+                offset | (mFlag ? 1 : 0), id);
+        try {
+            fragmentHeader.writeToByteBuffer(buffer);
+        } catch (IllegalArgumentException | BufferOverflowException e) {
+            throw new IOException("Error writing to buffer: ", e);
+        }
+    }
+
     /**
      * Finalize the packet.
      *
@@ -219,9 +241,31 @@
      */
     @NonNull
     public ByteBuffer finalizePacket() throws IOException {
+        // If the packet is finalized with L2 mtu greater than or equal to its current size, it will
+        // either return a List of size 1 or throw an IOException if something goes wrong.
+        return finalizePacket(mBuffer.position()).get(0);
+    }
+
+    /**
+     * Finalizes the packet with specified link MTU.
+     *
+     * Call after writing L4 header (no payload) or L4 payload to the buffer used by the builder.
+     * L3 header length, L3 header checksum and L4 header checksum are calculated and written back
+     * after finalization.
+     *
+     * @param l2mtu the maximum size, in bytes, of each individual packet. If the packet size
+     *              exceeds the l2mtu, it will be fragmented into smaller packets.
+     * @return a list of packet(s), each containing a portion of the original L3 payload.
+     */
+    @NonNull
+    public List<ByteBuffer> finalizePacket(int l2mtu) throws IOException {
         // [1] Finalize IPv4 or IPv6 header.
         int ipHeaderOffset = INVALID_OFFSET;
         if (mIpv4HeaderOffset != INVALID_OFFSET) {
+            if (mBuffer.position() > l2mtu) {
+                throw new IOException("IPv4 fragmentation is not supported");
+            }
+
             ipHeaderOffset = mIpv4HeaderOffset;
 
             // Populate the IPv4 totalLength field.
@@ -243,12 +287,15 @@
         }
 
         // [2] Finalize TCP or UDP header.
+        final int ipPayloadOffset;
         if (mTcpHeaderOffset != INVALID_OFFSET) {
+            ipPayloadOffset = mTcpHeaderOffset;
             // Populate the TCP header checksum field.
             mBuffer.putShort(mTcpHeaderOffset + TCP_CHECKSUM_OFFSET, tcpChecksum(mBuffer,
                     ipHeaderOffset /* ipOffset */, mTcpHeaderOffset /* transportOffset */,
                     mBuffer.position() - mTcpHeaderOffset /* transportLen */));
         } else if (mUdpHeaderOffset != INVALID_OFFSET) {
+            ipPayloadOffset = mUdpHeaderOffset;
             // Populate the UDP header length field.
             mBuffer.putShort(mUdpHeaderOffset + UDP_LENGTH_OFFSET,
                     (short) (mBuffer.position() - mUdpHeaderOffset));
@@ -257,11 +304,81 @@
             mBuffer.putShort(mUdpHeaderOffset + UDP_CHECKSUM_OFFSET, udpChecksum(mBuffer,
                     ipHeaderOffset /* ipOffset */, mUdpHeaderOffset /* transportOffset */));
         } else {
-            throw new IOException("Packet is missing neither TCP nor UDP header");
+            throw new IOException("Packet has neither TCP nor UDP header");
         }
 
-        mBuffer.flip();
-        return mBuffer;
+        if (mBuffer.position() <= l2mtu) {
+            mBuffer.flip();
+            return Arrays.asList(mBuffer);
+        }
+
+        // IPv6 Packet is fragmented into multiple smaller packets that would fit within the link
+        // MTU.
+        // Refer to https://tools.ietf.org/html/rfc2460
+        //
+        // original packet:
+        // +------------------+--------------+--------------+--//--+----------+
+        // |  Unfragmentable  |    first     |    second    |      |   last   |
+        // |       Part       |   fragment   |   fragment   | .... | fragment |
+        // +------------------+--------------+--------------+--//--+----------+
+        //
+        // fragment packets:
+        // +------------------+--------+--------------+
+        // |  Unfragmentable  |Fragment|    first     |
+        // |       Part       | Header |   fragment   |
+        // +------------------+--------+--------------+
+        //
+        // +------------------+--------+--------------+
+        // |  Unfragmentable  |Fragment|    second    |
+        // |       Part       | Header |   fragment   |
+        // +------------------+--------+--------------+
+        //                       o
+        //                       o
+        //                       o
+        // +------------------+--------+----------+
+        // |  Unfragmentable  |Fragment|   last   |
+        // |       Part       | Header | fragment |
+        // +------------------+--------+----------+
+        final List<ByteBuffer> fragments = new ArrayList<>();
+        final int totalPayloadLen = mBuffer.position() - ipPayloadOffset;
+        final int perPacketPayloadLen = l2mtu - ipPayloadOffset - IPV6_FRAGMENT_HEADER_LEN;
+        final short protocol = (short) Byte.toUnsignedInt(
+                mBuffer.get(mIpv6HeaderOffset + IPV6_PROTOCOL_OFFSET));
+        Random random = new Random();
+        final int id = random.nextInt(Integer.MAX_VALUE);
+        int startOffset = 0;
+        // Copy the packet content to a byte array.
+        byte[] packet = new byte[mBuffer.position()];
+        // The ByteBuffer#get(int index, byte[] dst) method is only available in API level 35 and
+        // above. Here, we use a more primitive approach: reposition the ByteBuffer to the beginning
+        // before copying, then return its position to the end afterward.
+        mBuffer.position(0);
+        mBuffer.get(packet);
+        mBuffer.position(packet.length);
+        while (startOffset < totalPayloadLen) {
+            int copyPayloadLen = Math.min(perPacketPayloadLen, totalPayloadLen - startOffset);
+            // The data portion must be broken into segments aligned with 8-octet boundaries.
+            // Therefore, the payload length should be a multiple of 8 bytes for all fragments
+            // except the last one.
+            // See https://datatracker.ietf.org/doc/html/rfc791 section 3.2
+            if (copyPayloadLen != totalPayloadLen - startOffset) {
+                copyPayloadLen &= ~7;
+            }
+            ByteBuffer fragment = ByteBuffer.allocate(ipPayloadOffset + IPV6_FRAGMENT_HEADER_LEN
+                    + copyPayloadLen);
+            fragment.put(packet, 0, ipPayloadOffset);
+            writeFragmentHeader(fragment, protocol, startOffset,
+                    startOffset + copyPayloadLen < totalPayloadLen, id);
+            fragment.put(packet, ipPayloadOffset + startOffset, copyPayloadLen);
+            fragment.putShort(mIpv6HeaderOffset + IPV6_LEN_OFFSET,
+                    (short) (IPV6_FRAGMENT_HEADER_LEN + copyPayloadLen));
+            fragment.put(mIpv6HeaderOffset + IPV6_PROTOCOL_OFFSET, (byte) IPPROTO_FRAGMENT);
+            fragment.flip();
+            fragments.add(fragment);
+            startOffset += copyPayloadLen;
+        }
+
+        return fragments;
     }
 
     /**
diff --git a/framework/src/android/net/RoutingCoordinatorManager.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
similarity index 95%
rename from framework/src/android/net/RoutingCoordinatorManager.java
rename to staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
index a9e7eef..02e3643 100644
--- a/framework/src/android/net/RoutingCoordinatorManager.java
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package android.net;
+package com.android.net.module.util;
 
 import android.content.Context;
-import android.os.Build;
+import android.net.RouteInfo;
+import android.os.IBinder;
 import android.os.RemoteException;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 
 /**
  * A manager class for talking to the routing coordinator service.
@@ -30,15 +30,14 @@
  * by the build rules. Do not change build rules to gain access to this class from elsewhere.
  * @hide
  */
-@RequiresApi(Build.VERSION_CODES.S)
 public class RoutingCoordinatorManager {
     @NonNull final Context mContext;
     @NonNull final IRoutingCoordinator mService;
 
     public RoutingCoordinatorManager(@NonNull final Context context,
-            @NonNull final IRoutingCoordinator service) {
+            @NonNull final IBinder binder) {
         mContext = context;
-        mService = service;
+        mService = IRoutingCoordinator.Stub.asInterface(binder);
     }
 
     /**
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
similarity index 98%
rename from service/src/com/android/server/connectivity/RoutingCoordinatorService.java
rename to staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
index 742a2cc..c75b860 100644
--- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorService.java
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.server.connectivity;
+package com.android.net.module.util;
 
 import static com.android.net.module.util.NetdUtils.toRouteInfoParcel;
 
 import android.annotation.NonNull;
 import android.net.INetd;
-import android.net.IRoutingCoordinator;
+
 import android.net.RouteInfo;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
diff --git a/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
new file mode 100644
index 0000000..a638cc4
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
@@ -0,0 +1,147 @@
+/*
+ * 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.net.module.util;
+
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.Pair;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.HashMap;
+import java.util.NoSuchElementException;
+
+/**
+ * Subclass of BpfMap for maps that are only ever written by one userspace writer.
+ *
+ * This class stores all map data in a userspace HashMap in addition to in the BPF map. This makes
+ * reads (but not iterations) much faster because they do not require a system call or converting
+ * the raw map read to the Value struct. See, e.g., b/343166906 .
+ *
+ * Users of this class must ensure that no BPF program ever writes to the map, and that all
+ * userspace writes to the map occur through this object. Other userspace code may still read from
+ * the map; only writes are required to go through this object.
+ *
+ * Reads and writes to this object are thread-safe and internally synchronized. The read and write
+ * methods are synchronized to ensure that current writers always result in a consistent internal
+ * state (without synchronization, two concurrent writes might update the underlying map and the
+ * cache in the opposite order, resulting in the cache being out of sync with the map).
+ *
+ * getNextKey and iteration over the map are not synchronized or cached and always access the
+ * isunderlying map. The values returned by these calls may be temporarily out of sync with the
+ * values read and written through this object.
+ *
+ * TODO: consider caching reads on iterations as well. This is not trivial because the semantics for
+ * iterating BPF maps require passing in the previously-returned key, and Java iterators only
+ * support iterating from the beginning. It could be done by implementing forEach and possibly by
+ * making getFirstKey and getNextKey private (if no callers are using them). Because HashMap is not
+ * thread-safe, implementing forEach would require either making that method synchronized (and
+ * block reads and updates from other threads until iteration is complete) or switching the
+ * internal HashMap to ConcurrentHashMap.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class SingleWriterBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
+    // HashMap instead of ArrayMap because it performs better on larger maps, and many maps used in
+    // our code can contain hundreds of items.
+    @GuardedBy("this")
+    private final HashMap<K, V> mCache = new HashMap<>();
+
+    // This should only ever be called (hence private) once for a given 'path'.
+    // Java-wise what matters is the entire {path, key, value} triplet,
+    // but of course the kernel exclusive lock is just on the path (fd),
+    // and any BpfMap has (or should have...) well defined key/value types
+    // (or at least their sizes) so in practice it doesn't really matter.
+    private SingleWriterBpfMap(@NonNull final String path, final Class<K> key,
+            final Class<V> value) throws ErrnoException, NullPointerException {
+        super(path, BPF_F_RDWR_EXCLUSIVE, key, value);
+
+        // Populate cache with the current map contents.
+        synchronized (this) {
+            K currentKey = super.getFirstKey();
+            while (currentKey != null) {
+                mCache.put(currentKey, super.getValue(currentKey));
+                currentKey = super.getNextKey(currentKey);
+            }
+        }
+    }
+
+    // This allows reuse of SingleWriterBpfMap objects for the same {path, keyClass, valueClass}.
+    // These are never destroyed, so once created the lock is (effectively) held till process death
+    // (even if fixed, there would still be a write-only fd cache in underlying BpfMap base class).
+    private static final HashMap<Pair<String, Pair<Class, Class>>, SingleWriterBpfMap>
+            singletonCache = new HashMap<>();
+
+    // This is the public 'factory method' that (creates if needed and) returns a singleton instance
+    // for a given map.  This holds an exclusive lock and has a permanent write-through cache.
+    // It will not be released until process death (or at least unload of the relevant class loader)
+    public synchronized static <KK extends Struct, VV extends Struct> SingleWriterBpfMap<KK,VV>
+            getSingleton(@NonNull final String path, final Class<KK> key, final Class<VV> value)
+            throws ErrnoException, NullPointerException {
+        var cacheKey = new Pair<>(path, new Pair<Class,Class>(key, value));
+        if (!singletonCache.containsKey(cacheKey))
+            singletonCache.put(cacheKey, new SingleWriterBpfMap(path, key, value));
+        return singletonCache.get(cacheKey);
+    }
+
+    @Override
+    public synchronized void updateEntry(K key, V value) throws ErrnoException {
+        super.updateEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized void insertEntry(K key, V value)
+            throws ErrnoException, IllegalStateException {
+        super.insertEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized void replaceEntry(K key, V value)
+            throws ErrnoException, NoSuchElementException {
+        super.replaceEntry(key, value);
+        mCache.put(key, value);
+    }
+
+    @Override
+    public synchronized boolean insertOrReplaceEntry(K key, V value) throws ErrnoException {
+        final boolean ret = super.insertOrReplaceEntry(key, value);
+        mCache.put(key, value);
+        return ret;
+    }
+
+    @Override
+    public synchronized boolean deleteEntry(K key) throws ErrnoException {
+        final boolean ret = super.deleteEntry(key);
+        mCache.remove(key);
+        return ret;
+    }
+
+    @Override
+    public synchronized boolean containsKey(@NonNull K key) throws ErrnoException {
+        return mCache.containsKey(key);
+    }
+
+    @Override
+    public synchronized V getValue(@NonNull K key) throws ErrnoException {
+        return mCache.get(key);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/SocketUtils.java b/staticlibs/device/com/android/net/module/util/SocketUtils.java
index 5e6a6c6..51671a6 100644
--- a/staticlibs/device/com/android/net/module/util/SocketUtils.java
+++ b/staticlibs/device/com/android/net/module/util/SocketUtils.java
@@ -19,8 +19,7 @@
 import static android.net.util.SocketUtils.closeSocket;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresApi;
-import android.os.Build;
+import android.annotation.SuppressLint;
 import android.system.NetlinkSocketAddress;
 
 import java.io.FileDescriptor;
@@ -41,7 +40,11 @@
     /**
      * Make a socket address to communicate with netlink.
      */
-    @NonNull @RequiresApi(Build.VERSION_CODES.S)
+    // NetlinkSocketAddress was CorePlatformApi on R and linter warns this is available on S+.
+    // android.net.util.SocketUtils.makeNetlinkSocketAddress can be used instead, but this method
+    // has been used on R, so suppress the linter and keep as it is.
+    @SuppressLint("NewApi")
+    @NonNull
     public static SocketAddress makeNetlinkSocketAddress(int portId, int groupsMask) {
         return new NetlinkSocketAddress(portId, groupsMask);
     }
diff --git a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
similarity index 96%
rename from Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
rename to staticlibs/device/com/android/net/module/util/SyncStateMachine.java
index a17eb26..da184d3 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
+++ b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.networkstack.tethering.util;
+package com.android.net.module.util;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -165,6 +165,11 @@
      * The message is processed sequentially, so calling this method recursively is not permitted.
      * In other words, using this method inside State#enter, State#exit, or State#processMessage
      * is incorrect and will result in an IllegalStateException.
+     *
+     * @param what is assigned to Message.what
+     * @param arg1 is assigned to Message.arg1
+     * @param arg2 is assigned to Message.arg2
+     * @param obj  is assigned to Message.obj
      */
     public final void processMessage(int what, int arg1, int arg2, @Nullable Object obj) {
         ensureCorrectThread();
@@ -189,6 +194,15 @@
         mCurrentlyProcessing = Integer.MIN_VALUE;
     }
 
+    /**
+     * Synchronously process a message and perform state transition.
+     *
+     * @param what is assigned to Message.what.
+     */
+    public final void processMessage(int what) {
+        processMessage(what, 0, 0, null);
+    }
+
     private void maybeProcessSelfMessageQueue() {
         while (!mSelfMsgQueue.isEmpty()) {
             currentStateProcessMessageThenPerformTransitions(mSelfMsgQueue.poll());
diff --git a/staticlibs/device/com/android/net/module/util/TcUtils.java b/staticlibs/device/com/android/net/module/util/TcUtils.java
index 9d2fb7f..a6b222f 100644
--- a/staticlibs/device/com/android/net/module/util/TcUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TcUtils.java
@@ -101,4 +101,12 @@
      * @throws IOException
      */
     public static native void tcQdiscAddDevClsact(int ifIndex) throws IOException;
+
+    /**
+     * Attempt to fetch a bpf program from a path.
+     * Return true on success, false on non-existence or any other failure.
+     *
+     * @param bpfProgPath
+     */
+    public static native boolean isBpfProgramUsable(String bpfProgPath);
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NdOption.java b/staticlibs/device/com/android/net/module/util/netlink/NdOption.java
index defc88a..4f58380 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NdOption.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NdOption.java
@@ -67,6 +67,9 @@
             case StructNdOptRdnss.TYPE:
                 return StructNdOptRdnss.parse(buf);
 
+            case StructNdOptPio.TYPE:
+                return StructNdOptPio.parse(buf);
+
             default:
                 int newPosition = Math.min(buf.limit(), buf.position() + length * 8);
                 buf.position(newPosition);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
index 2e9a99b..586f19d 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
@@ -156,8 +156,9 @@
 
     @Override
     public String toString() {
-        return String.format("Nduseroptmsg(%d, %d, %d, %d, %d, %s)",
+        return String.format("Nduseroptmsg(family:%d, opts_len:%d, ifindex:%d, icmp_type:%d, "
+                + "icmp_code:%d, srcaddr: %s, %s)",
                 family, opts_len, ifindex, Byte.toUnsignedInt(icmp_type),
-                Byte.toUnsignedInt(icmp_code), srcaddr.getHostAddress());
+                Byte.toUnsignedInt(icmp_code), srcaddr.getHostAddress(), option);
     }
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index ad7a4d7..1896de6 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -123,6 +123,7 @@
     public static final short RTM_NEWRULE                   = 32;
     public static final short RTM_DELRULE                   = 33;
     public static final short RTM_GETRULE                   = 34;
+    public static final short RTM_NEWPREFIX                 = 52;
     public static final short RTM_NEWNDUSEROPT              = 68;
 
     // Netfilter netlink message types are presented by two bytes: high byte subsystem and
@@ -148,6 +149,8 @@
     public static final int RTMGRP_IPV4_IFADDR = 0x10;
     public static final int RTMGRP_IPV6_IFADDR = 0x100;
     public static final int RTMGRP_IPV6_ROUTE  = 0x400;
+    public static final int RTNLGRP_IPV6_PREFIX = 18;
+    public static final int RTMGRP_IPV6_PREFIX = 1 << (RTNLGRP_IPV6_PREFIX - 1);
     public static final int RTNLGRP_ND_USEROPT = 20;
     public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
 
@@ -207,6 +210,7 @@
             case RTM_NEWRULE: return "RTM_NEWRULE";
             case RTM_DELRULE: return "RTM_DELRULE";
             case RTM_GETRULE: return "RTM_GETRULE";
+            case RTM_NEWPREFIX: return "RTM_NEWPREFIX";
             case RTM_NEWNDUSEROPT: return "RTM_NEWNDUSEROPT";
             default: return "unknown RTM type: " + String.valueOf(nlmType);
         }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index 92ec0c4..0c49edc 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -41,6 +41,10 @@
     public static final short IFLA_ADDRESS   = 1;
     public static final short IFLA_IFNAME    = 3;
     public static final short IFLA_MTU       = 4;
+    public static final short IFLA_INET6_ADDR_GEN_MODE = 8;
+    public static final short IFLA_AF_SPEC = 26;
+
+    public static final short IN6_ADDR_GEN_MODE_NONE = 1;
 
     private int mMtu;
     @NonNull
@@ -58,6 +62,17 @@
         mInterfaceName = null;
     }
 
+    @VisibleForTesting
+    public RtNetlinkLinkMessage(@NonNull StructNlMsgHdr nlmsghdr,
+            int mtu, @NonNull StructIfinfoMsg ifinfomsg, @NonNull MacAddress hardwareAddress,
+            @NonNull String interfaceName) {
+        super(nlmsghdr);
+        mMtu = mtu;
+        mIfinfomsg = ifinfomsg;
+        mHardwareAddress = hardwareAddress;
+        mInterfaceName = interfaceName;
+    }
+
     public int getMtu() {
         return mMtu;
     }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java
index 02d1574..28eeaea 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructIfinfoMsg.java
@@ -49,7 +49,7 @@
     @Field(order = 4, type = Type.U32)
     public final long change;
 
-    StructIfinfoMsg(short family, int type, int index, long flags, long change) {
+    public StructIfinfoMsg(short family, int type, int index, long flags, long change) {
         this.family = family;
         this.type = type;
         this.index = index;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPio.java b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPio.java
new file mode 100644
index 0000000..65541eb
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPio.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import android.net.IpPrefix;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.PrefixInformationOption;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * The Prefix Information Option. RFC 4861.
+ *
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |     Type      |    Length     | Prefix Length |L|A|R|P| Rsvd1 |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                         Valid Lifetime                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                       Preferred Lifetime                      |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                           Reserved2                           |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +                            Prefix                             +
+ * |                                                               |
+ * +                                                               +
+ * |                                                               |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class StructNdOptPio extends NdOption {
+    private static final String TAG = StructNdOptPio.class.getSimpleName();
+    public static final int TYPE = 3;
+    public static final byte LENGTH = 4; // Length in 8-byte units
+
+    public final byte flags;
+    public final long preferred;
+    public final long valid;
+    @NonNull
+    public final IpPrefix prefix;
+
+    public StructNdOptPio(byte flags, long preferred, long valid, @NonNull final IpPrefix prefix) {
+        super((byte) TYPE, LENGTH);
+        this.prefix = Objects.requireNonNull(prefix, "prefix must not be null");
+        this.flags = flags;
+        this.preferred = preferred;
+        this.valid = valid;
+    }
+
+    /**
+     * Parses a PrefixInformation option from a {@link ByteBuffer}.
+     *
+     * @param buf The buffer from which to parse the option. The buffer's byte order must be
+     *            {@link java.nio.ByteOrder#BIG_ENDIAN}.
+     * @return the parsed option, or {@code null} if the option could not be parsed successfully.
+     */
+    public static StructNdOptPio parse(@NonNull ByteBuffer buf) {
+        if (buf == null || buf.remaining() < LENGTH * 8) return null;
+        try {
+            final PrefixInformationOption pio = Struct.parse(PrefixInformationOption.class, buf);
+            if (pio.type != TYPE) {
+                throw new IllegalArgumentException("Invalid type " + pio.type);
+            }
+            if (pio.length != LENGTH) {
+                throw new IllegalArgumentException("Invalid length " + pio.length);
+            }
+            return new StructNdOptPio(pio.flags, pio.preferredLifetime, pio.validLifetime,
+                    pio.getIpPrefix());
+        } catch (IllegalArgumentException | BufferUnderflowException e) {
+            // Not great, but better than throwing an exception that might crash the caller.
+            // Convention in this package is that null indicates that the option was truncated
+            // or malformed, so callers must already handle it.
+            Log.d(TAG, "Invalid PIO option: " + e);
+            return null;
+        }
+    }
+
+    protected void writeToByteBuffer(ByteBuffer buf) {
+        buf.put(PrefixInformationOption.build(prefix, flags, valid, preferred));
+    }
+
+    /** Outputs the wire format of the option to a new big-endian ByteBuffer. */
+    public ByteBuffer toByteBuffer() {
+        final ByteBuffer buf = ByteBuffer.allocate(Struct.getSize(PrefixInformationOption.class));
+        writeToByteBuffer(buf);
+        buf.flip();
+        return buf;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return String.format("NdOptPio(flags:%s, preferred lft:%s, valid lft:%s, prefix:%s)",
+                HexDump.toHexString(flags), preferred, valid, prefix);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructPrefixCacheInfo.java b/staticlibs/device/com/android/net/module/util/netlink/StructPrefixCacheInfo.java
new file mode 100644
index 0000000..cfaa6e1
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructPrefixCacheInfo.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct prefix_cacheinfo {
+ *     __u32 preferred_time;
+ *     __u32 valid_time;
+ * }
+ *
+ * see also:
+ *
+ *     include/uapi/linux/if_addr.h
+ *
+ * @hide
+ */
+public class StructPrefixCacheInfo extends Struct {
+    public static final int STRUCT_SIZE = 8;
+
+    @Field(order = 0, type = Type.U32)
+    public final long preferred_time;
+    @Field(order = 1, type = Type.U32)
+    public final long valid_time;
+
+    StructPrefixCacheInfo(long preferred, long valid) {
+        this.preferred_time = preferred;
+        this.valid_time = valid;
+    }
+
+    /**
+     * Parse a prefix_cacheinfo struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the prefix_cacheinfo.
+     * @return the parsed prefix_cacheinfo struct, or throw IllegalArgumentException if the
+     *         prefix_cacheinfo struct could not be parsed successfully(for example, if it was
+     *         truncated).
+     */
+    public static StructPrefixCacheInfo parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) {
+            throw new IllegalArgumentException("Invalid bytebuffer remaining size "
+                    + byteBuffer.remaining() + " for prefix_cacheinfo attribute");
+        }
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructPrefixCacheInfo.class, byteBuffer);
+    }
+
+    /**
+     * Write a prefix_cacheinfo struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructPrefixMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructPrefixMsg.java
new file mode 100644
index 0000000..504d6c7
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructPrefixMsg.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct prefixmsg {
+ *     unsigned char  prefix_family;
+ *     unsigned char  prefix_pad1;
+ *     unsigned short prefix_pad2;
+ *     int            prefix_ifindex;
+ *     unsigned char  prefix_type;
+ *     unsigned char  prefix_len;
+ *     unsigned char  prefix_flags;
+ *     unsigned char  prefix_pad3;
+ * }
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class StructPrefixMsg extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 12;
+
+    @Field(order = 0, type = Type.U8, padding = 3)
+    public final short prefix_family;
+    @Field(order = 1, type = Type.S32)
+    public final int prefix_ifindex;
+    @Field(order = 2, type = Type.U8)
+    public final short prefix_type;
+    @Field(order = 3, type = Type.U8)
+    public final short prefix_len;
+    @Field(order = 4, type = Type.U8, padding = 1)
+    public final short prefix_flags;
+
+    @VisibleForTesting
+    public StructPrefixMsg(short family, int ifindex, short type, short len, short flags) {
+        this.prefix_family = family;
+        this.prefix_ifindex = ifindex;
+        this.prefix_type = type;
+        this.prefix_len = len;
+        this.prefix_flags = flags;
+    }
+
+    /**
+     * Parse a prefixmsg struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the prefixmsg.
+     * @return the parsed prefixmsg struct, or throw IllegalArgumentException if the prefixmsg
+     *         struct could not be parsed successfully (for example, if it was truncated).
+     */
+    public static StructPrefixMsg parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) {
+            throw new IllegalArgumentException("Invalid bytebuffer remaining size "
+                    + byteBuffer.remaining() + "for prefix_msg struct.");
+        }
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructPrefixMsg.class, byteBuffer);
+    }
+
+    /**
+     * Write a prefixmsg struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/FragmentHeader.java b/staticlibs/device/com/android/net/module/util/structs/FragmentHeader.java
new file mode 100644
index 0000000..3da6a38
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/FragmentHeader.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * IPv6 Fragment Extension header, as per https://tools.ietf.org/html/rfc2460.
+ *
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |  Next Header  |   Reserved    |      Fragment Offset    |Res|M|
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                         Identification                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class FragmentHeader extends Struct {
+    @Field(order = 0, type = Type.U8)
+    public final short nextHeader;
+    @Field(order = 1, type = Type.S8)
+    public final byte reserved;
+    @Field(order = 2, type = Type.U16)
+    public final int fragmentOffset;
+    @Field(order = 3, type = Type.S32)
+    public final int identification;
+
+    public FragmentHeader(final short nextHeader, final byte reserved, final int fragmentOffset,
+            final int identification) {
+        this.nextHeader = nextHeader;
+        this.reserved = reserved;
+        this.fragmentOffset = fragmentOffset;
+        this.identification = identification;
+    }
+
+    public FragmentHeader(final short nextHeader, final int fragmentOffset,
+            final int identification) {
+        this(nextHeader, (byte) 0, fragmentOffset, identification);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java b/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
index 49d7654..bbbe571 100644
--- a/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
@@ -21,11 +21,16 @@
 import android.net.IpPrefix;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Computed;
 import com.android.net.module.util.Struct.Field;
 import com.android.net.module.util.Struct.Type;
 
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 
@@ -71,7 +76,11 @@
     @Field(order = 7, type = Type.ByteArray, arraysize = 16)
     public final byte[] prefix;
 
-    PrefixInformationOption(final byte type, final byte length, final byte prefixLen,
+    @Computed
+    private final IpPrefix mIpPrefix;
+
+    @VisibleForTesting
+    public PrefixInformationOption(final byte type, final byte length, final byte prefixLen,
             final byte flags, final long validLifetime, final long preferredLifetime,
             final int reserved, @NonNull final byte[] prefix) {
         this.type = type;
@@ -82,6 +91,23 @@
         this.preferredLifetime = preferredLifetime;
         this.reserved = reserved;
         this.prefix = prefix;
+
+        try {
+            final Inet6Address addr = (Inet6Address) InetAddress.getByAddress(prefix);
+            mIpPrefix = new IpPrefix(addr, prefixLen);
+        } catch (UnknownHostException | ClassCastException e) {
+            // UnknownHostException should never happen unless prefix is null.
+            // ClassCastException can occur when prefix is an IPv6 mapped IPv4 address.
+            // Both scenarios should throw an exception in the context of Struct#parse().
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Return the prefix {@link IpPrefix} included in the PIO.
+     */
+    public IpPrefix getIpPrefix() {
+        return mIpPrefix;
     }
 
     /**
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index 39e7ce9..f3d8c4a 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -389,4 +389,28 @@
         }
         return dest;
     }
+
+    /**
+     * Returns an index of the given SparseArray that contains the given value, or -1
+     * number if no keys map to the given value.
+     *
+     * <p>Note this is a linear search, and if multiple keys can map to the same value
+     * then the smallest index is returned.
+     *
+     * <p>This function compares values with {@code equals} while the
+     * {@link SparseArray#indexOfValue} compares values using {@code ==}.
+     */
+    public static <T> int getIndexForValue(SparseArray<T> sparseArray, T value) {
+        for(int i = 0, nsize = sparseArray.size(); i < nsize; i++) {
+            T valueAt = sparseArray.valueAt(i);
+            if (valueAt == null) {
+                if (value == null) {
+                    return i;
+                };
+            } else if (valueAt.equals(value)) {
+                return i;
+            }
+        }
+        return -1;
+    }
 }
diff --git a/staticlibs/framework/com/android/net/module/util/DnsUtils.java b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
new file mode 100644
index 0000000..19ffd72
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Dns common utility functions.
+ *
+ * @hide
+ */
+public class DnsUtils {
+
+    private DnsUtils() { }
+
+    /**
+     * Convert the string to DNS case-insensitive uppercase.
+     *
+     * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to
+     * their unaccented counterparts. So the "DNS uppercase" should be if character is a-z then they
+     * transform into A-Z. Otherwise, they are kept as-is.
+     */
+    public static String toDnsUpperCase(@NonNull String string) {
+        final char[] outChars = new char[string.length()];
+        for (int i = 0; i < string.length(); i++) {
+            outChars[i] = toDnsUpperCase(string.charAt(i));
+        }
+        return new String(outChars);
+    }
+
+    /**
+     * Convert the array of labels to DNS case-insensitive uppercase.
+     */
+    public static String[] toDnsLabelsUpperCase(@NonNull String[] labels) {
+        final String[] outStrings = new String[labels.length];
+        for (int i = 0; i < labels.length; ++i) {
+            outStrings[i] = toDnsUpperCase(labels[i]);
+        }
+        return outStrings;
+    }
+
+    /**
+     * Compare two strings by DNS case-insensitive uppercase.
+     */
+    public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
+        if (a == null || b == null) {
+            return a == null && b == null;
+        }
+        if (a.length() != b.length()) return false;
+        for (int i = 0; i < a.length(); i++) {
+            if (toDnsUpperCase(a.charAt(i)) != toDnsUpperCase(b.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Compare two set of DNS labels by DNS case-insensitive uppercase.
+     */
+    public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
+        if (a == b) {
+            return true;
+        }
+        int length = a.length;
+        if (b.length != length) {
+            return false;
+        }
+        for (int i = 0; i < length; i++) {
+            if (!equalsIgnoreDnsCase(a[i], b[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static char toDnsUpperCase(char a) {
+        return a >= 'a' && a <= 'z' ? (char) (a - ('a' - 'A')) : a;
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
index 54ce01e..7066131 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
@@ -39,12 +39,13 @@
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
 import static android.net.NetworkCapabilities.TRANSPORT_USB;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 
-import static com.android.net.module.util.BitUtils.packBitList;
+import static com.android.net.module.util.BitUtils.packBits;
 import static com.android.net.module.util.BitUtils.unpackBits;
 
 import android.annotation.NonNull;
@@ -75,8 +76,8 @@
         TRANSPORT_BLUETOOTH,
         TRANSPORT_WIFI,
         TRANSPORT_ETHERNET,
-        TRANSPORT_USB
-
+        TRANSPORT_USB,
+        TRANSPORT_SATELLITE
         // Notably, TRANSPORT_TEST is not in this list as any network that has TRANSPORT_TEST and
         // one of the above transports should be counted as that transport, to keep tests as
         // realistic as possible.
@@ -88,41 +89,41 @@
       * and {@code FORCE_RESTRICTED_CAPABILITIES}.
      */
     @VisibleForTesting
-    public static final long RESTRICTED_CAPABILITIES = packBitList(
-            NET_CAPABILITY_BIP,
-            NET_CAPABILITY_CBS,
-            NET_CAPABILITY_DUN,
-            NET_CAPABILITY_EIMS,
-            NET_CAPABILITY_ENTERPRISE,
-            NET_CAPABILITY_FOTA,
-            NET_CAPABILITY_IA,
-            NET_CAPABILITY_IMS,
-            NET_CAPABILITY_MCX,
-            NET_CAPABILITY_RCS,
-            NET_CAPABILITY_VEHICLE_INTERNAL,
-            NET_CAPABILITY_VSIM,
-            NET_CAPABILITY_XCAP,
-            NET_CAPABILITY_MMTEL);
+    public static final long RESTRICTED_CAPABILITIES =
+            (1L << NET_CAPABILITY_BIP) |
+            (1L << NET_CAPABILITY_CBS) |
+            (1L << NET_CAPABILITY_DUN) |
+            (1L << NET_CAPABILITY_EIMS) |
+            (1L << NET_CAPABILITY_ENTERPRISE) |
+            (1L << NET_CAPABILITY_FOTA) |
+            (1L << NET_CAPABILITY_IA) |
+            (1L << NET_CAPABILITY_IMS) |
+            (1L << NET_CAPABILITY_MCX) |
+            (1L << NET_CAPABILITY_RCS) |
+            (1L << NET_CAPABILITY_VEHICLE_INTERNAL) |
+            (1L << NET_CAPABILITY_VSIM) |
+            (1L << NET_CAPABILITY_XCAP) |
+            (1L << NET_CAPABILITY_MMTEL);
 
     /**
      * Capabilities that force network to be restricted.
      * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
      */
-    private static final long FORCE_RESTRICTED_CAPABILITIES = packBitList(
-            NET_CAPABILITY_ENTERPRISE,
-            NET_CAPABILITY_OEM_PAID,
-            NET_CAPABILITY_OEM_PRIVATE);
+    private static final long FORCE_RESTRICTED_CAPABILITIES =
+            (1L << NET_CAPABILITY_ENTERPRISE) |
+            (1L << NET_CAPABILITY_OEM_PAID) |
+            (1L << NET_CAPABILITY_OEM_PRIVATE);
 
     /**
      * Capabilities that suggest that a network is unrestricted.
      * See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
      */
     @VisibleForTesting
-    public static final long UNRESTRICTED_CAPABILITIES = packBitList(
-            NET_CAPABILITY_INTERNET,
-            NET_CAPABILITY_MMS,
-            NET_CAPABILITY_SUPL,
-            NET_CAPABILITY_WIFI_P2P);
+    public static final long UNRESTRICTED_CAPABILITIES =
+            (1L << NET_CAPABILITY_INTERNET) |
+            (1L << NET_CAPABILITY_MMS) |
+            (1L << NET_CAPABILITY_SUPL) |
+            (1L << NET_CAPABILITY_WIFI_P2P);
 
     /**
      * Get a transport that can be used to classify a network when displaying its info to users.
@@ -158,28 +159,33 @@
      *
      * @return {@code true} if the network should be restricted.
      */
-    // TODO: Use packBits(nc.getCapabilities()) to check more easily using bit masks.
     public static boolean inferRestrictedCapability(NetworkCapabilities nc) {
+        return inferRestrictedCapability(packBits(nc.getCapabilities()));
+    }
+
+    /**
+     * Infers that all the capabilities it provides are typically provided by restricted networks
+     * or not.
+     *
+     * @param capabilities see {@link NetworkCapabilities#getCapabilities()}
+     *
+     * @return {@code true} if the network should be restricted.
+     */
+    public static boolean inferRestrictedCapability(long capabilities) {
         // Check if we have any capability that forces the network to be restricted.
-        for (int capability : unpackBits(FORCE_RESTRICTED_CAPABILITIES)) {
-            if (nc.hasCapability(capability)) {
-                return true;
-            }
+        if ((capabilities & FORCE_RESTRICTED_CAPABILITIES) != 0) {
+            return true;
         }
 
         // Verify there aren't any unrestricted capabilities.  If there are we say
         // the whole thing is unrestricted unless it is forced to be restricted.
-        for (int capability : unpackBits(UNRESTRICTED_CAPABILITIES)) {
-            if (nc.hasCapability(capability)) {
-                return false;
-            }
+        if ((capabilities & UNRESTRICTED_CAPABILITIES) != 0) {
+            return false;
         }
 
         // Must have at least some restricted capabilities.
-        for (int capability : unpackBits(RESTRICTED_CAPABILITIES)) {
-            if (nc.hasCapability(capability)) {
-                return true;
-            }
+        if ((capabilities & RESTRICTED_CAPABILITIES) != 0) {
+            return true;
         }
         return false;
     }
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 7c4abe0..f1ff2e4 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -129,7 +129,9 @@
     public static final int IPV6_PROTOCOL_OFFSET = 6;
     public static final int IPV6_SRC_ADDR_OFFSET = 8;
     public static final int IPV6_DST_ADDR_OFFSET = 24;
+    public static final int IPV6_FRAGMENT_ID_OFFSET = 4;
     public static final int IPV6_MIN_MTU = 1280;
+    public static final int IPV6_FRAGMENT_ID_LEN = 4;
     public static final int IPV6_FRAGMENT_HEADER_LEN = 8;
     public static final int RFC7421_PREFIX_LENGTH = 64;
     // getSockOpt() for v6 MTU
@@ -141,6 +143,8 @@
     public static final Inet6Address IPV6_ADDR_ALL_HOSTS_MULTICAST =
             (Inet6Address) InetAddresses.parseNumericAddress("ff02::3");
 
+    public static final int IPPROTO_FRAGMENT = 44;
+
     /**
      * ICMP constants.
      *
@@ -179,6 +183,8 @@
     public static final int ICMPV6_RA_HEADER_LEN = 16;
     public static final int ICMPV6_NS_HEADER_LEN = 24;
     public static final int ICMPV6_NA_HEADER_LEN = 24;
+    public static final int ICMPV6_ND_OPTION_TLLA_LEN = 8;
+    public static final int ICMPV6_ND_OPTION_SLLA_LEN = 8;
 
     public static final int NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER    = 1 << 31;
     public static final int NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED = 1 << 30;
@@ -254,6 +260,18 @@
     public static final short DNS_OVER_TLS_PORT = 853;
 
     /**
+     * Dns query type constants.
+     *
+     * See also:
+     *    - https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.2
+     */
+    public static final int TYPE_A = 1;
+    public static final int TYPE_PTR = 12;
+    public static final int TYPE_TXT = 16;
+    public static final int TYPE_AAAA = 28;
+    public static final int TYPE_SRV = 33;
+
+    /**
      * IEEE802.11 standard constants.
      *
      * See also:
diff --git a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
index 463b0c4..98d91a5 100644
--- a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
+++ b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
@@ -87,7 +87,9 @@
         }
     }
 
-    @VisibleForTesting
+    /**
+     * Get the current counter value for the given uid.
+     */
     public synchronized int get(int uid) {
         return mUidToCount.get(uid, 0);
     }
diff --git a/staticlibs/lint-baseline.xml b/staticlibs/lint-baseline.xml
deleted file mode 100644
index 2ee3a43..0000000
--- a/staticlibs/lint-baseline.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha04" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha04">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `makeNetlinkSocketAddress`"
-        errorLine1="            Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));"
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java"
-            line="111"
-            column="25"/>
-    </issue>
-
-</issues>
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
index d716358..4bcd259 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -20,6 +20,7 @@
 #include <android-base/unique_fd.h>
 #include <linux/bpf.h>
 #include <poll.h>
+#include <sys/epoll.h>
 #include <sys/mman.h>
 #include <utils/Log.h>
 
@@ -33,7 +34,7 @@
 // BpfRingbufBase contains the non-templated functionality of BPF ring buffers.
 class BpfRingbufBase {
  public:
-  ~BpfRingbufBase() {
+  virtual ~BpfRingbufBase() {
     if (mConsumerPos) munmap(mConsumerPos, mConsumerSize);
     if (mProducerPos) munmap(mProducerPos, mProducerSize);
     mConsumerPos = nullptr;
@@ -139,19 +140,31 @@
   static base::Result<std::unique_ptr<BpfRingbuf<Value>>> Create(
       const char* path);
 
+  int epoll_ctl_add(int epfd, struct epoll_event *event) {
+    return epoll_ctl(epfd, EPOLL_CTL_ADD, mRingFd.get(), event);
+  }
+
+  int epoll_ctl_mod(int epfd, struct epoll_event *event) {
+    return epoll_ctl(epfd, EPOLL_CTL_MOD, mRingFd.get(), event);
+  }
+
+  int epoll_ctl_del(int epfd) {
+    return epoll_ctl(epfd, EPOLL_CTL_DEL, mRingFd.get(), NULL);
+  }
+
   // Consumes all messages from the ring buffer, passing them to the callback.
   // Returns the number of messages consumed or a non-ok result on error. If the
   // ring buffer has no pending messages an OK result with count 0 is returned.
   base::Result<int> ConsumeAll(const MessageCallback& callback);
 
- private:
+ protected:
   // Empty ctor for use by Create.
   BpfRingbuf() : BpfRingbufBase(sizeof(Value)) {}
 };
 
 
 inline base::Result<void> BpfRingbufBase::Init(const char* path) {
-  mRingFd.reset(mapRetrieveRW(path));
+  mRingFd.reset(mapRetrieveExclusiveRW(path));
   if (!mRingFd.ok()) {
     return android::base::ErrnoError()
            << "failed to retrieve ringbuffer at " << path;
diff --git a/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h b/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
index 59257b8..417a5c4 100644
--- a/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
+++ b/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
@@ -42,10 +42,26 @@
     return kver;
 }
 
-static inline __unused bool isAtLeastKernelVersion(unsigned major, unsigned minor, unsigned sub) {
+static inline bool isAtLeastKernelVersion(unsigned major, unsigned minor, unsigned sub) {
     return kernelVersion() >= KVER(major, minor, sub);
 }
 
+static inline bool isKernelVersion(unsigned major, unsigned minor) {
+    return isAtLeastKernelVersion(major, minor, 0) && !isAtLeastKernelVersion(major, minor + 1, 0);
+}
+
+static inline bool __unused isLtsKernel() {
+    return isKernelVersion(4,  4) ||  // minimum for Android R
+           isKernelVersion(4,  9) ||  // minimum for Android S & T
+           isKernelVersion(4, 14) ||  // minimum for Android U
+           isKernelVersion(4, 19) ||  // minimum for Android V
+           isKernelVersion(5,  4) ||  // first supported in Android R
+           isKernelVersion(5, 10) ||  // first supported in Android S
+           isKernelVersion(5, 15) ||  // first supported in Android T
+           isKernelVersion(6,  1) ||  // first supported in Android U
+           isKernelVersion(6,  6);    // first supported in Android V
+}
+
 // Figure out the bitness of userspace.
 // Trivial and known at compile time.
 static constexpr bool isUserspace32bit() {
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index baff09b..4ddec8b 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -37,7 +37,30 @@
 #define BPFLOADER_IGNORED_ON_VERSION 33u
 
 // Android U / 14 (api level 34) - various new program types added
-#define BPFLOADER_U_VERSION 37u
+#define BPFLOADER_U_VERSION 38u
+
+// Android U QPR2 / 14 (api level 34) - platform only
+// (note: the platform bpfloader in V isn't really versioned at all,
+//  as there is no need as it can only load objects compiled at the
+//  same time as itself and the rest of the platform)
+#define BPFLOADER_U_QPR2_VERSION 41u
+#define BPFLOADER_PLATFORM_VERSION BPFLOADER_U_QPR2_VERSION
+
+// Android Mainline - this bpfloader should eventually go back to T (or even S)
+// Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
+#define BPFLOADER_MAINLINE_VERSION 42u
+
+// Android Mainline BpfLoader when running on Android T
+#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_VERSION + 1u)
+
+// Android Mainline BpfLoader when running on Android U
+#define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
+
+// Android Mainline BpfLoader when running on Android U QPR3
+#define BPFLOADER_MAINLINE_U_QPR3_VERSION (BPFLOADER_MAINLINE_U_VERSION + 1u)
+
+// Android Mainline BpfLoader when running on Android V
+#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
 
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
@@ -48,7 +71,7 @@
  * In which case it's just best to use the default.
  */
 #ifndef BPFLOADER_MIN_VER
-#define BPFLOADER_MIN_VER COMPILE_FOR_BPFLOADER_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION
 #endif
 
 #ifndef BPFLOADER_MAX_VER
@@ -111,10 +134,13 @@
 #define KVER_NONE KVER_(0)
 #define KVER_4_14 KVER(4, 14, 0)
 #define KVER_4_19 KVER(4, 19, 0)
-#define KVER_5_4 KVER(5, 4, 0)
-#define KVER_5_8 KVER(5, 8, 0)
-#define KVER_5_9 KVER(5, 9, 0)
+#define KVER_5_4  KVER(5, 4, 0)
+#define KVER_5_8  KVER(5, 8, 0)
+#define KVER_5_9  KVER(5, 9, 0)
+#define KVER_5_10 KVER(5, 10, 0)
 #define KVER_5_15 KVER(5, 15, 0)
+#define KVER_6_1  KVER(6, 1, 0)
+#define KVER_6_6  KVER(6, 6, 0)
 #define KVER_INF KVER_(0xFFFFFFFFu)
 
 #define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index ef03c4d..00ef91a 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -48,9 +48,6 @@
 #define DEFAULT_SIZEOF_BPF_MAP_DEF 32       // v0.0 struct: enum (uint sized) + 7 uint
 #define DEFAULT_SIZEOF_BPF_PROG_DEF 20      // v0.0 struct: 4 uint + bool + 3 byte alignment pad
 
-// By default, unless otherwise specified, allow the use of features only supported by v0.37.
-#define COMPILE_FOR_BPFLOADER_VERSION 37u
-
 /*
  * The bpf_{map,prog}_def structures are compiled for different architectures.
  * Once by the BPF compiler for the BPF architecture, and once by a C++
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
index 9995cb9..73cef89 100644
--- a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
@@ -16,8 +16,11 @@
 
 #pragma once
 
+#include <stdlib.h>
+#include <unistd.h>
 #include <linux/bpf.h>
 #include <linux/unistd.h>
+#include <sys/file.h>
 
 #ifdef BPF_FD_JUST_USE_INT
   #define BPF_FD_TYPE int
@@ -128,26 +131,74 @@
                             });
 }
 
-inline int mapRetrieve(const char* pathname, uint32_t flag) {
-    return bpfFdGet(pathname, flag);
+int bpfGetFdMapId(const BPF_FD_TYPE map_fd);
+
+inline int bpfLock(int fd, short type) {
+    if (fd < 0) return fd;  // pass any errors straight through
+#ifdef BPF_MAP_LOCKLESS_FOR_TEST
+    return fd;
+#endif
+#ifdef BPF_FD_JUST_USE_INT
+    int mapId = bpfGetFdMapId(fd);
+    int saved_errno = errno;
+#else
+    base::unique_fd ufd(fd);
+    int mapId = bpfGetFdMapId(ufd);
+    int saved_errno = errno;
+    (void)ufd.release();
+#endif
+    // 4.14+ required to fetch map id, but we don't want to call isAtLeastKernelVersion
+    if (mapId == -1 && saved_errno == EINVAL) return fd;
+    if (mapId <= 0) abort();  // should not be possible
+
+    // on __LP64__ (aka. 64-bit userspace) 'struct flock64' is the same as 'struct flock'
+    struct flock64 fl = {
+        .l_type = type,        // short: F_{RD,WR,UN}LCK
+        .l_whence = SEEK_SET,  // short: SEEK_{SET,CUR,END}
+        .l_start = mapId,      // off_t: start offset
+        .l_len = 1,            // off_t: number of bytes
+    };
+
+    // see: bionic/libc/bionic/fcntl.cpp: iff !__LP64__ this uses fcntl64
+    int ret = fcntl(fd, F_OFD_SETLK, &fl);
+    if (!ret) return fd;  // success
+    close(fd);
+    return ret;  // most likely -1 with errno == EAGAIN, due to already held lock
+}
+
+inline int mapRetrieveLocklessRW(const char* pathname) {
+    return bpfFdGet(pathname, 0);
+}
+
+inline int mapRetrieveExclusiveRW(const char* pathname) {
+    return bpfLock(mapRetrieveLocklessRW(pathname), F_WRLCK);
 }
 
 inline int mapRetrieveRW(const char* pathname) {
-    return mapRetrieve(pathname, 0);
+    return bpfLock(mapRetrieveLocklessRW(pathname), F_RDLCK);
 }
 
 inline int mapRetrieveRO(const char* pathname) {
-    return mapRetrieve(pathname, BPF_F_RDONLY);
+    return bpfFdGet(pathname, BPF_F_RDONLY);
 }
 
+// WARNING: it's impossible to grab a shared (ie. read) lock on a write-only fd,
+// so we instead choose to grab an exclusive (ie. write) lock.
 inline int mapRetrieveWO(const char* pathname) {
-    return mapRetrieve(pathname, BPF_F_WRONLY);
+    return bpfLock(bpfFdGet(pathname, BPF_F_WRONLY), F_WRLCK);
 }
 
 inline int retrieveProgram(const char* pathname) {
     return bpfFdGet(pathname, BPF_F_RDONLY);
 }
 
+inline bool usableProgram(const char* pathname) {
+    int fd = retrieveProgram(pathname);
+    bool ok = (fd >= 0);
+    if (ok) close(fd);
+    return ok;
+}
+
 inline int attachProgram(bpf_attach_type type, const BPF_FD_TYPE prog_fd,
                          const BPF_FD_TYPE cg_fd, uint32_t flags = 0) {
     return bpf(BPF_PROG_ATTACH, {
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
index 7e6b4ec..969ebd4 100644
--- a/staticlibs/native/bpfmapjni/Android.bp
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -39,7 +39,7 @@
         "-Werror",
         "-Wno-unused-parameter",
     ],
-    sdk_version: "30",
+    sdk_version: "current",
     min_sdk_version: "30",
     apex_available: [
         "com.android.tethering",
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
index b92f107..1923ceb 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -35,7 +35,24 @@
         jstring path, jint mode, jint keySize, jint valueSize) {
     ScopedUtfChars pathname(env, path);
 
-    jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast<unsigned>(mode));
+    jint fd = -1;
+    switch (mode) {
+      case 0:
+        fd = bpf::mapRetrieveRW(pathname.c_str());
+        break;
+      case BPF_F_RDONLY:
+        fd = bpf::mapRetrieveRO(pathname.c_str());
+        break;
+      case BPF_F_WRONLY:
+        fd = bpf::mapRetrieveWO(pathname.c_str());
+        break;
+      case BPF_F_RDONLY|BPF_F_WRONLY:
+        fd = bpf::mapRetrieveExclusiveRW(pathname.c_str());
+        break;
+      default:
+        errno = EINVAL;
+        break;
+    }
 
     if (fd < 0) {
         jniThrowErrnoException(env, "nativeBpfFdGet", errno);
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
index ab83da6..2a587b6 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
@@ -19,6 +19,9 @@
 #include <nativehelper/scoped_utf_chars.h>
 #include <tcutils/tcutils.h>
 
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+
 namespace android {
 
 static void throwIOException(JNIEnv *env, const char *msg, int error) {
@@ -96,6 +99,14 @@
   }
 }
 
+static jboolean com_android_net_module_util_TcUtils_isBpfProgramUsable(JNIEnv *env,
+                                                                       jclass clazz,
+                                                                       jstring bpfProgPath) {
+    ScopedUtfChars pathname(env, bpfProgPath);
+    return bpf::usableProgram(pathname.c_str());
+}
+
+
 /*
  * JNI registration.
  */
@@ -111,6 +122,8 @@
      (void *)com_android_net_module_util_TcUtils_tcFilterDelDev},
     {"tcQdiscAddDevClsact", "(I)V",
      (void *)com_android_net_module_util_TcUtils_tcQdiscAddDevClsact},
+    {"isBpfProgramUsable", "(Ljava/lang/String;)Z",
+     (void *)com_android_net_module_util_TcUtils_isBpfProgramUsable},
 };
 
 int register_com_android_net_module_util_TcUtils(JNIEnv *env,
diff --git a/staticlibs/native/tcutils/Android.bp b/staticlibs/native/tcutils/Android.bp
index 926590d..e4742ce 100644
--- a/staticlibs/native/tcutils/Android.bp
+++ b/staticlibs/native/tcutils/Android.bp
@@ -21,7 +21,10 @@
     name: "libtcutils",
     srcs: ["tcutils.cpp"],
     export_include_dirs: ["include"],
-    header_libs: ["bpf_headers"],
+    header_libs: [
+        "bpf_headers",
+        "libbase_headers",
+    ],
     shared_libs: [
         "liblog",
     ],
@@ -31,7 +34,7 @@
         "-Werror",
         "-Wno-unused-parameter",
     ],
-    sdk_version: "30",
+    sdk_version: "current",
     min_sdk_version: "30",
     apex_available: [
         "com.android.tethering",
diff --git a/staticlibs/native/tcutils/scopeguard.h b/staticlibs/native/tcutils/scopeguard.h
deleted file mode 100644
index 76bbb93..0000000
--- a/staticlibs/native/tcutils/scopeguard.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.
- */
-
-// -----------------------------------------------------------------------------
-// TODO: figure out a way to use libbase_ndk. This is currently not working
-// because of missing apex availability. For now, we can use a copy of
-// ScopeGuard which is very lean compared to unique_fd. This code has been
-// copied verbatim from:
-// https://cs.android.com/android/platform/superproject/+/master:system/libbase/include/android-base/scopeguard.h
-
-#pragma once
-
-#include <utility> // for std::move, std::forward
-
-namespace android {
-namespace base {
-
-// ScopeGuard ensures that the specified functor is executed no matter how the
-// current scope exits.
-template <typename F> class ScopeGuard {
-public:
-  ScopeGuard(F &&f) : f_(std::forward<F>(f)), active_(true) {}
-
-  ScopeGuard(ScopeGuard &&that) noexcept
-      : f_(std::move(that.f_)), active_(that.active_) {
-    that.active_ = false;
-  }
-
-  template <typename Functor>
-  ScopeGuard(ScopeGuard<Functor> &&that)
-      : f_(std::move(that.f_)), active_(that.active_) {
-    that.active_ = false;
-  }
-
-  ~ScopeGuard() {
-    if (active_)
-      f_();
-  }
-
-  ScopeGuard() = delete;
-  ScopeGuard(const ScopeGuard &) = delete;
-  void operator=(const ScopeGuard &) = delete;
-  void operator=(ScopeGuard &&that) = delete;
-
-  void Disable() { active_ = false; }
-
-  bool active() const { return active_; }
-
-private:
-  template <typename Functor> friend class ScopeGuard;
-
-  F f_;
-  bool active_;
-};
-
-template <typename F> ScopeGuard<F> make_scope_guard(F &&f) {
-  return ScopeGuard<F>(std::forward<F>(f));
-}
-
-} // namespace base
-} // namespace android
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
index c82390f..21e781c 100644
--- a/staticlibs/native/tcutils/tcutils.cpp
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -20,8 +20,10 @@
 
 #include "logging.h"
 #include "bpf/KernelUtils.h"
-#include "scopeguard.h"
 
+#include <BpfSyscallWrappers.h>
+#include <android-base/scopeguard.h>
+#include <android-base/unique_fd.h>
 #include <arpa/inet.h>
 #include <cerrno>
 #include <cstring>
@@ -39,10 +41,6 @@
 #include <unistd.h>
 #include <utility>
 
-#define BPF_FD_JUST_USE_INT
-#include <BpfSyscallWrappers.h>
-#undef BPF_FD_JUST_USE_INT
-
 // The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c.
 #define CLS_BPF_NAME_LEN 256
 
@@ -52,6 +50,9 @@
 namespace android {
 namespace {
 
+using base::make_scope_guard;
+using base::unique_fd;
+
 /**
  * IngressPoliceFilterBuilder builds a nlmsg request equivalent to the following
  * tc command:
@@ -130,7 +131,7 @@
   // class members
   const unsigned mBurstInBytes;
   const char *mBpfProgPath;
-  int mBpfFd;
+  unique_fd mBpfFd;
   Request mRequest;
 
   static double getTickInUsec() {
@@ -139,7 +140,7 @@
       ALOGE("fopen(\"/proc/net/psched\"): %s", strerror(errno));
       return 0.0;
     }
-    auto scopeGuard = base::make_scope_guard([fp] { fclose(fp); });
+    auto scopeGuard = make_scope_guard([fp] { fclose(fp); });
 
     uint32_t t2us;
     uint32_t us2t;
@@ -166,7 +167,6 @@
                       unsigned burstInBytes, const char* bpfProgPath)
       : mBurstInBytes(burstInBytes),
         mBpfProgPath(bpfProgPath),
-        mBpfFd(-1),
         mRequest{
             .n = {
                 .nlmsg_len = sizeof(mRequest),
@@ -298,13 +298,6 @@
   }
   // clang-format on
 
-  ~IngressPoliceFilterBuilder() {
-    // TODO: use unique_fd
-    if (mBpfFd != -1) {
-      close(mBpfFd);
-    }
-  }
-
   constexpr unsigned getRequestSize() const { return sizeof(Request); }
 
 private:
@@ -332,14 +325,14 @@
   }
 
   int initBpfFd() {
-    mBpfFd = bpf::retrieveProgram(mBpfProgPath);
-    if (mBpfFd == -1) {
+    mBpfFd.reset(bpf::retrieveProgram(mBpfProgPath));
+    if (!mBpfFd.ok()) {
       int error = errno;
       ALOGE("retrieveProgram failed: %d", error);
       return -error;
     }
 
-    mRequest.opt.acts.act2.opt.fd.u32 = static_cast<uint32_t>(mBpfFd);
+    mRequest.opt.acts.act2.opt.fd.u32 = static_cast<uint32_t>(mBpfFd.get());
     snprintf(mRequest.opt.acts.act2.opt.name.str,
              sizeof(mRequest.opt.acts.act2.opt.name.str), "%s:[*fsobj]",
              basename(mBpfProgPath));
@@ -370,14 +363,13 @@
 
 int sendAndProcessNetlinkResponse(const void *req, int len) {
   // TODO: use unique_fd instead of ScopeGuard
-  int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
-  if (fd == -1) {
+  unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
+  if (!fd.ok()) {
     int error = errno;
     ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %d",
              error);
     return -error;
   }
-  auto scopeGuard = base::make_scope_guard([fd] { close(fd); });
 
   static constexpr int on = 1;
   if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) {
@@ -460,10 +452,9 @@
 }
 
 int hardwareAddressType(const char *interface) {
-  int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
-  if (fd < 0)
+  unique_fd fd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0));
+  if (!fd.ok())
     return -errno;
-  auto scopeGuard = base::make_scope_guard([fd] { close(fd); });
 
   struct ifreq ifr = {};
   // We use strncpy() instead of strlcpy() since kernel has to be able
@@ -576,12 +567,11 @@
 // /sys/fs/bpf/... direct-action
 int tcAddBpfFilter(int ifIndex, bool ingress, uint16_t prio, uint16_t proto,
                    const char *bpfProgPath) {
-  const int bpfFd = bpf::retrieveProgram(bpfProgPath);
-  if (bpfFd == -1) {
+  unique_fd bpfFd(bpf::retrieveProgram(bpfProgPath));
+  if (!bpfFd.ok()) {
     ALOGE("retrieveProgram failed: %d", errno);
     return -errno;
   }
-  auto scopeGuard = base::make_scope_guard([bpfFd] { close(bpfFd); });
 
   struct {
     nlmsghdr n;
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 59ef20d..44abba2 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -22,7 +22,7 @@
     sdk_version: "system_current",
     min_sdk_version: "30",
     static_libs: [
-        "netd_aidl_interface-V14-java",
+        "netd_aidl_interface-V15-java",
     ],
     apex_available: [
         "//apex_available:platform", // used from services.net
@@ -45,7 +45,7 @@
 cc_library_static {
     name: "netd_aidl_interface-lateststable-ndk",
     whole_static_libs: [
-        "netd_aidl_interface-V14-ndk",
+        "netd_aidl_interface-V15-ndk",
     ],
     apex_available: [
         "com.android.resolv",
@@ -56,12 +56,12 @@
 
 cc_defaults {
     name: "netd_aidl_interface_lateststable_cpp_static",
-    static_libs: ["netd_aidl_interface-V14-cpp"],
+    static_libs: ["netd_aidl_interface-V15-cpp"],
 }
 
 cc_defaults {
     name: "netd_aidl_interface_lateststable_cpp_shared",
-    shared_libs: ["netd_aidl_interface-V14-cpp"],
+    shared_libs: ["netd_aidl_interface-V15-cpp"],
 }
 
 aidl_interface {
@@ -167,6 +167,10 @@
             version: "14",
             imports: [],
         },
+        {
+            version: "15",
+            imports: [],
+        },
 
     ],
     frozen: true,
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/15/.hash
new file mode 100644
index 0000000..afdadcc
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/.hash
@@ -0,0 +1 @@
+2be6ff6fb01645cdddb3bb60f6de5727e5733267
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/INetd.aidl
new file mode 100644
index 0000000..80b3b62
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/INetd.aidl
@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) 2016, 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetd {
+  boolean isAlive();
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+  boolean bandwidthEnableDataSaver(boolean enable);
+  /**
+   * @deprecated use networkCreate() instead.
+   */
+  void networkCreatePhysical(int netId, int permission);
+  /**
+   * @deprecated use networkCreate() instead.
+   */
+  void networkCreateVpn(int netId, boolean secure);
+  void networkDestroy(int netId);
+  void networkAddInterface(int netId, in @utf8InCpp String iface);
+  void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+  void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+  void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+  void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+  void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+  boolean tetherApplyDnsInterfaces();
+  android.net.TetherStatsParcel[] tetherGetStats();
+  void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+  void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+  @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+  void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+  void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+  int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+  void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+  void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+  void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+  void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+  void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+  void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+  void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+  void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+  void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+  void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+  void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+  void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+  void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+  void strictUidCleartextPenalty(int uid, int policyPenalty);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+   */
+  @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+   */
+  void clatdStop(in @utf8InCpp String ifName);
+  boolean ipfwdEnabled();
+  @utf8InCpp String[] ipfwdGetRequesterList();
+  void ipfwdEnableForwarding(in @utf8InCpp String requester);
+  void ipfwdDisableForwarding(in @utf8InCpp String requester);
+  void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+  void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+  void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+  void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+  void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+  void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+  void bandwidthSetGlobalAlert(long bytes);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthAddNaughtyApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthRemoveNaughtyApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthAddNiceApp(int uid);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void bandwidthRemoveNiceApp(int uid);
+  void tetherStart(in @utf8InCpp String[] dhcpRanges);
+  void tetherStop();
+  boolean tetherIsEnabled();
+  void tetherInterfaceAdd(in @utf8InCpp String ifName);
+  void tetherInterfaceRemove(in @utf8InCpp String ifName);
+  @utf8InCpp String[] tetherInterfaceList();
+  void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+  @utf8InCpp String[] tetherDnsList();
+  void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+  void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+  void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+  void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+  int networkGetDefault();
+  void networkSetDefault(int netId);
+  void networkClearDefault();
+  void networkSetPermissionForNetwork(int netId, int permission);
+  void networkSetPermissionForUser(int permission, in int[] uids);
+  void networkClearPermissionForUser(in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void trafficSetNetPermForUids(int permission, in int[] uids);
+  void networkSetProtectAllow(int uid);
+  void networkSetProtectDeny(int uid);
+  boolean networkCanProtect(int uid);
+  void firewallSetFirewallType(int firewalltype);
+  void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallSetUidRule(int childChain, int uid, int firewallRule);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallEnableChildChain(int childChain, boolean enable);
+  @utf8InCpp String[] interfaceGetList();
+  android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+  void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+  void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+  void interfaceClearAddrs(in @utf8InCpp String ifName);
+  void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+  void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+  void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+  void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+  void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+  void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void firewallRemoveUidInterfaceRules(in int[] uids);
+  /**
+   * @deprecated unimplemented on T+.
+   */
+  void trafficSwapActiveStatsMap();
+  IBinder getOemNetd();
+  void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+  android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+  void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  android.net.TetherStatsParcel[] tetherOffloadGetStats();
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+  /**
+   * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+   */
+  android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+  void networkCreate(in android.net.NativeNetworkConfig config);
+  void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+  void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+  void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+  void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+  void networkAllowBypassVpnOnNetwork(boolean allow, int uid, int netId);
+  const int IPV4 = 4;
+  const int IPV6 = 6;
+  const int CONF = 1;
+  const int NEIGH = 2;
+  const String IPSEC_INTERFACE_PREFIX = "ipsec";
+  const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+  const int IPV6_ADDR_GEN_MODE_NONE = 1;
+  const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+  const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+  const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+  const int PENALTY_POLICY_ACCEPT = 1;
+  const int PENALTY_POLICY_LOG = 2;
+  const int PENALTY_POLICY_REJECT = 3;
+  const int CLAT_MARK = 0xdeadc1a7;
+  const int LOCAL_NET_ID = 99;
+  const int DUMMY_NET_ID = 51;
+  const int UNREACHABLE_NET_ID = 52;
+  const String NEXTHOP_NONE = "";
+  const String NEXTHOP_UNREACHABLE = "unreachable";
+  const String NEXTHOP_THROW = "throw";
+  const int PERMISSION_NONE = 0;
+  const int PERMISSION_NETWORK = 1;
+  const int PERMISSION_SYSTEM = 2;
+  const int NO_PERMISSIONS = 0;
+  const int PERMISSION_INTERNET = 4;
+  const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+  const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
+  /**
+   * @deprecated use FIREWALL_ALLOWLIST.
+   */
+  const int FIREWALL_WHITELIST = 0;
+  const int FIREWALL_ALLOWLIST = 0;
+  /**
+   * @deprecated use FIREWALL_DENYLIST.
+   */
+  const int FIREWALL_BLACKLIST = 1;
+  const int FIREWALL_DENYLIST = 1;
+  const int FIREWALL_RULE_ALLOW = 1;
+  const int FIREWALL_RULE_DENY = 2;
+  const int FIREWALL_CHAIN_NONE = 0;
+  const int FIREWALL_CHAIN_DOZABLE = 1;
+  const int FIREWALL_CHAIN_STANDBY = 2;
+  const int FIREWALL_CHAIN_POWERSAVE = 3;
+  const int FIREWALL_CHAIN_RESTRICTED = 4;
+  const String IF_STATE_UP = "up";
+  const String IF_STATE_DOWN = "down";
+  const String IF_FLAG_BROADCAST = "broadcast";
+  const String IF_FLAG_LOOPBACK = "loopback";
+  const String IF_FLAG_POINTOPOINT = "point-to-point";
+  const String IF_FLAG_RUNNING = "running";
+  const String IF_FLAG_MULTICAST = "multicast";
+  const int IPSEC_DIRECTION_IN = 0;
+  const int IPSEC_DIRECTION_OUT = 1;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+  oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+  oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+  oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+  oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+  oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+  oneway void onInterfaceAdded(@utf8InCpp String ifName);
+  oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+  oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+  oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+  oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+  oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+  @utf8InCpp String ifName;
+  @utf8InCpp String hwAddr;
+  @utf8InCpp String ipv4Addr;
+  int prefixLength;
+  @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/IpSecMigrateInfoParcel.aidl
@@ -0,0 +1,45 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+  int requestId;
+  int selAddrFamily;
+  int direction;
+  @utf8InCpp String oldSourceAddress;
+  @utf8InCpp String oldDestinationAddress;
+  @utf8InCpp String newSourceAddress;
+  @utf8InCpp String newDestinationAddress;
+  int interfaceId;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable MarkMaskParcel {
+  int mark;
+  int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+  int netId;
+  android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+  int permission;
+  boolean secure;
+  android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+  boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeNetworkType.aidl
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeNetworkType {
+  PHYSICAL = 0,
+  VIRTUAL = 1,
+  PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeVpnType {
+  SERVICE = 1,
+  PLATFORM = 2,
+  LEGACY = 3,
+  OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable RouteInfoParcel {
+  @utf8InCpp String destination;
+  @utf8InCpp String ifName;
+  @utf8InCpp String nextHop;
+  int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherConfigParcel {
+  boolean usingLegacyDnsProxy;
+  @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+  int inputInterfaceIndex;
+  int outputInterfaceIndex;
+  byte[] destination;
+  int prefixLength;
+  byte[] srcL2Address;
+  byte[] dstL2Address;
+  int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherStatsParcel {
+  @utf8InCpp String iface;
+  long rxBytes;
+  long rxPackets;
+  long txBytes;
+  long txPackets;
+  int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+  int start;
+  int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/15/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+  int netId;
+  android.net.UidRangeParcel[] uidRanges;
+  int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
index 8ccefb2..80b3b62 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
@@ -203,6 +203,7 @@
   void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
   void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
   void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+  void networkAllowBypassVpnOnNetwork(boolean allow, int uid, int netId);
   const int IPV4 = 4;
   const int IPV6 = 6;
   const int CONF = 1;
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
index ee27e84..e4c63b9 100644
--- a/staticlibs/netd/binder/android/net/INetd.aidl
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -1446,4 +1446,27 @@
     *                               - subPriority: unused
     */
     void setNetworkAllowlist(in NativeUidRangeConfig[] allowedNetworks);
+
+    /**
+     * Allow the UID to explicitly select the given network even if it is subject to a VPN.
+     *
+     * Throws ServiceSpecificException with error code EEXISTS when trying to add a bypass rule that
+     * already exists, and ENOENT when trying to remove a bypass rule that does not exist.
+     *
+     * netId specific bypass rules can be combined and are allowed to overlap with global VPN
+     * exclusions (by calling networkSetProtectAllow / networkSetProtectDeny, or by setting netId to
+     * 0). Adding or removing global VPN bypass rules does not affect the netId specific rules and
+     * vice versa.
+     *
+     * Note that if netId is set to 0 (NETID_UNSET) this API is equivalent to
+     * networkSetProtectAllow} / #networkSetProtectDeny.
+     *
+     * @param allow whether to allow or disallow the operation.
+     * @param uid the UID
+     * @param netId the netId that the UID is allowed to select.
+     *
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *         cause of the failure.
+     */
+    void networkAllowBypassVpnOnNetwork(boolean allow, int uid, int netId);
 }
diff --git a/staticlibs/netd/libnetdutils/InternetAddresses.cpp b/staticlibs/netd/libnetdutils/InternetAddresses.cpp
index 322f1b1..6d98608 100644
--- a/staticlibs/netd/libnetdutils/InternetAddresses.cpp
+++ b/staticlibs/netd/libnetdutils/InternetAddresses.cpp
@@ -16,6 +16,7 @@
 
 #include "netdutils/InternetAddresses.h"
 
+#include <stdlib.h>
 #include <string>
 
 #include <android-base/stringprintf.h>
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
index d266cbc..2de5ed7 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Log.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -203,7 +203,7 @@
     void record(Level lvl, const std::string& entry);
 
     mutable std::shared_mutex mLock;
-    std::deque<const std::string> mEntries;  // GUARDED_BY(mLock), when supported
+    std::deque<std::string> mEntries;  // GUARDED_BY(mLock), when supported
 };
 
 }  // namespace netdutils
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 4c226cc..91f94b5 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -25,7 +25,9 @@
         "net-utils-device-common-async",
         "net-utils-device-common-bpf",
         "net-utils-device-common-ip",
+        "net-utils-device-common-struct-base",
         "net-utils-device-common-wear",
+        "net-utils-service-connectivity",
     ],
     libs: [
         "android.test.runner",
@@ -38,6 +40,7 @@
         "//packages/modules/NetworkStack/tests/integration",
     ],
     lint: {
+        strict_updatability_linting: true,
         test: true,
     },
 }
@@ -55,4 +58,33 @@
     ],
     jarjar_rules: "jarjar-rules.txt",
     test_suites: ["device-tests"],
+    lint: {
+        strict_updatability_linting: true,
+    },
+}
+
+python_test_host {
+    name: "NetworkStaticLibHostPythonTests",
+    srcs: [
+        "host/python/*.py",
+    ],
+    main: "host/python/run_tests.py",
+    libs: [
+        "mobly",
+        "net-tests-utils-host-python-common",
+    ],
+    test_config: "host/python/test_config.xml",
+    test_suites: [
+        "general-tests",
+    ],
+    // MoblyBinaryHostTest doesn't support unit_test.
+    test_options: {
+        unit_test: false,
+    },
+    // Needed for applying VirtualEnv.
+    version: {
+        py3: {
+            embedded_launcher: false,
+        },
+    },
 }
diff --git a/staticlibs/tests/unit/host/python/adb_utils_test.py b/staticlibs/tests/unit/host/python/adb_utils_test.py
new file mode 100644
index 0000000..8fcca37
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/adb_utils_test.py
@@ -0,0 +1,122 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from unittest.mock import MagicMock, patch
+from absl.testing import parameterized
+from mobly import asserts
+from mobly import base_test
+from mobly import config_parser
+from net_tests_utils.host.python import adb_utils
+from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
+
+
+class TestAdbUtils(base_test.BaseTestClass, parameterized.TestCase):
+
+  def __init__(self, configs: config_parser.TestRunConfig):
+    super().__init__(configs)
+
+  def setup_test(self):
+    self.mock_ad = MagicMock()  # Mock Android device object
+    self.mock_ad.log = MagicMock()
+    self.mock_ad.adb.shell.return_value = b""  # Default empty return for shell
+
+  @patch(
+      "net_tests_utils.host.python.adb_utils.expect_dumpsys_state_with_retry"
+  )
+  @patch("net_tests_utils.host.python.adb_utils._set_screen_state")
+  def test_set_doze_mode_enable(
+      self, mock_set_screen_state, mock_expect_dumpsys_state
+  ):
+    adb_utils.set_doze_mode(self.mock_ad, True)
+    mock_set_screen_state.assert_called_once_with(self.mock_ad, False)
+
+  @patch(
+      "net_tests_utils.host.python.adb_utils.expect_dumpsys_state_with_retry"
+  )
+  def test_set_doze_mode_disable(self, mock_expect_dumpsys_state):
+    adb_utils.set_doze_mode(self.mock_ad, False)
+
+  @patch("net_tests_utils.host.python.adb_utils._get_screen_state")
+  def test_set_screen_state_success(self, mock_get_screen_state):
+    mock_get_screen_state.side_effect = [False, True]  # Simulate toggle
+    adb_utils._set_screen_state(self.mock_ad, True)
+
+  @patch("net_tests_utils.host.python.adb_utils._get_screen_state")
+  def test_set_screen_state_failure(self, mock_get_screen_state):
+    mock_get_screen_state.return_value = False  # State doesn't change
+    with asserts.assert_raises(UnexpectedBehaviorError):
+      adb_utils._set_screen_state(self.mock_ad, True)
+
+  @parameterized.parameters(
+      ("Awake", True),
+      ("Asleep", False),
+      ("Dozing", False),
+      ("SomeOtherState", False),
+  )  # Declare inputs for state_str and expected_result.
+  @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys")
+  def test_get_screen_state(self, state_str, expected_result, mock_get_value):
+    mock_get_value.return_value = state_str
+    asserts.assert_equal(
+        adb_utils._get_screen_state(self.mock_ad), expected_result
+    )
+
+  def test_get_value_of_key_from_dumpsys(self):
+    self.mock_ad.adb.shell.return_value = (
+        b"mWakefulness=Awake\nmOtherKey=SomeValue"
+    )
+    result = adb_utils.get_value_of_key_from_dumpsys(
+        self.mock_ad, "power", "mWakefulness"
+    )
+    asserts.assert_equal(result, "Awake")
+
+  @parameterized.parameters(
+      (True, ["true"]),
+      (False, ["false"]),
+      (
+          True,
+          ["false", "true"],
+      ),  # Expect True, get False which is unexpected, then get True
+      (
+          False,
+          ["true", "false"],
+      ),  # Expect False, get True which is unexpected, then get False
+  )  # Declare inputs for expected_state and returned_value
+  @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys")
+  def test_expect_dumpsys_state_with_retry_success(
+      self, expected_state, returned_value, mock_get_value
+  ):
+    mock_get_value.side_effect = returned_value
+    # Verify the method returns and does not throw.
+    adb_utils.expect_dumpsys_state_with_retry(
+        self.mock_ad, "service", "key", expected_state, 0
+    )
+
+  @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys")
+  def test_expect_dumpsys_state_with_retry_failure(self, mock_get_value):
+    mock_get_value.return_value = "false"
+    with asserts.assert_raises(UnexpectedBehaviorError):
+      adb_utils.expect_dumpsys_state_with_retry(
+          self.mock_ad, "service", "key", True, 0
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.get_value_of_key_from_dumpsys")
+  def test_expect_dumpsys_state_with_retry_not_found(self, mock_get_value):
+    # Simulate the get_value_of_key_from_dumpsys cannot find the give key.
+    mock_get_value.return_value = None
+
+    # Expect the function to raise UnexpectedBehaviorError due to the exception
+    with asserts.assert_raises(UnexpectedBehaviorError):
+      adb_utils.expect_dumpsys_state_with_retry(
+          self.mock_ad, "service", "key", True
+      )
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
new file mode 100644
index 0000000..b5a941b
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -0,0 +1,178 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from unittest.mock import MagicMock, patch
+from absl.testing import parameterized
+from mobly import asserts
+from mobly import base_test
+from mobly import config_parser
+from mobly.controllers.android_device_lib.adb import AdbError
+from net_tests_utils.host.python.apf_utils import (
+    ApfCapabilities,
+    PatternNotFoundException,
+    UnsupportedOperationException,
+    get_apf_capabilities,
+    get_apf_counter,
+    get_apf_counters_from_dumpsys,
+    get_hardware_address,
+    is_send_raw_packet_downstream_supported,
+    send_raw_packet_downstream,
+)
+from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
+
+TEST_IFACE_NAME = "eth0"
+TEST_PACKET_IN_HEX = "AABBCCDDEEFF"
+
+
+class TestApfUtils(base_test.BaseTestClass, parameterized.TestCase):
+
+  def __init__(self, configs: config_parser.TestRunConfig):
+    super().__init__(configs)
+
+  def setup_test(self):
+    self.mock_ad = MagicMock()  # Mock Android device object
+
+  @patch("net_tests_utils.host.python.adb_utils.get_dumpsys_for_service")
+  def test_get_apf_counters_from_dumpsys_success(
+      self, mock_get_dumpsys: MagicMock
+  ) -> None:
+    mock_get_dumpsys.return_value = """
+IpClient.wlan0
+  APF packet counters:
+    COUNTER_NAME1: 123
+    COUNTER_NAME2: 456
+"""
+    counters = get_apf_counters_from_dumpsys(self.mock_ad, "wlan0")
+    asserts.assert_equal(counters, {"COUNTER_NAME1": 123, "COUNTER_NAME2": 456})
+
+  @patch("net_tests_utils.host.python.adb_utils.get_dumpsys_for_service")
+  def test_get_apf_counters_from_dumpsys_exceptions(
+      self, mock_get_dumpsys: MagicMock
+  ) -> None:
+    test_cases = [
+        "",
+        "IpClient.wlan0\n",
+        "IpClient.wlan0\n APF packet counters:\n",
+        """
+IpClient.wlan1
+  APF packet counters:
+    COUNTER_NAME1: 123
+    COUNTER_NAME2: 456
+""",
+    ]
+
+    for dumpsys_output in test_cases:
+      mock_get_dumpsys.return_value = dumpsys_output
+      with asserts.assert_raises(PatternNotFoundException):
+        get_apf_counters_from_dumpsys(self.mock_ad, "wlan0")
+
+  @patch("net_tests_utils.host.python.apf_utils.get_apf_counters_from_dumpsys")
+  def test_get_apf_counter(self, mock_get_counters: MagicMock) -> None:
+    iface = "wlan0"
+    mock_get_counters.return_value = {
+        "COUNTER_NAME1": 123,
+        "COUNTER_NAME2": 456,
+    }
+    asserts.assert_equal(
+        get_apf_counter(self.mock_ad, iface, "COUNTER_NAME1"), 123
+    )
+    # Not found
+    asserts.assert_equal(
+        get_apf_counter(self.mock_ad, iface, "COUNTER_NAME3"), 0
+    )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_hardware_address_success(
+      self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.return_value = """
+46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
+ link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
+"""
+    mac_address = get_hardware_address(self.mock_ad, "wlan0")
+    asserts.assert_equal(mac_address, "72:05:77:82:21:E0")
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_hardware_address_not_found(
+      self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.return_value = "Some output without MAC address"
+    with asserts.assert_raises(PatternNotFoundException):
+      get_hardware_address(self.mock_ad, "wlan0")
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_send_raw_packet_downstream_success(
+      self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.return_value = ""  # Successful command output
+    send_raw_packet_downstream(
+        self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+    )
+    mock_adb_shell.assert_called_once_with(
+        self.mock_ad,
+        "cmd network_stack send-raw-packet-downstream"
+        f" {TEST_IFACE_NAME} {TEST_PACKET_IN_HEX}",
+    )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_send_raw_packet_downstream_failure(
+      self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.return_value = (  # Unexpected command output
+        "Any Unexpected Output"
+    )
+    with asserts.assert_raises(UnexpectedBehaviorError):
+      send_raw_packet_downstream(
+          self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+      )
+    asserts.assert_true(
+        is_send_raw_packet_downstream_supported(self.mock_ad),
+        "Send raw packet should be supported.",
+    )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_send_raw_packet_downstream_unsupported(
+      self, mock_adb_shell: MagicMock
+  ) -> None:
+    mock_adb_shell.side_effect = AdbError(
+        cmd="", stdout="Unknown command", stderr="", ret_code=3
+    )
+    with asserts.assert_raises(UnsupportedOperationException):
+      send_raw_packet_downstream(
+          self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+      )
+    asserts.assert_false(
+        is_send_raw_packet_downstream_supported(self.mock_ad),
+        "Send raw packet should not be supported.",
+    )
+
+  @parameterized.parameters(
+      ("2,2048,1", ApfCapabilities(2, 2048, 1)),  # Valid input
+      ("3,1024,0", ApfCapabilities(3, 1024, 0)),  # Valid input
+      ("invalid,output", ApfCapabilities(0, 0, 0)),  # Invalid input
+      ("", ApfCapabilities(0, 0, 0)),  # Empty input
+  )
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_apf_capabilities(
+      self, mock_output, expected_result, mock_adb_shell
+  ):
+    """Tests the get_apf_capabilities function with various inputs and expected results."""
+    # Configure the mock adb_shell to return the specified output
+    mock_adb_shell.return_value = mock_output
+
+    # Call the function under test
+    result = get_apf_capabilities(self.mock_ad, "wlan0")
+
+    # Assert that the result matches the expected result
+    asserts.assert_equal(result, expected_result)
diff --git a/staticlibs/tests/unit/host/python/assert_utils_test.py b/staticlibs/tests/unit/host/python/assert_utils_test.py
new file mode 100644
index 0000000..7a33373
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/assert_utils_test.py
@@ -0,0 +1,94 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import asserts
+from mobly import base_test
+from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError, expect_with_retry
+
+
+class TestAssertUtils(base_test.BaseTestClass):
+
+  def test_predicate_succeed(self):
+    """Test when the predicate becomes True within retries."""
+    call_count = 0
+
+    def predicate():
+      nonlocal call_count
+      call_count += 1
+      return call_count > 2  # True on the third call
+
+    expect_with_retry(predicate, max_retries=5, retry_interval_sec=0)
+    asserts.assert_equal(call_count, 3)  # Ensure it was called exactly 3 times
+
+  def test_predicate_failed(self):
+    """Test when the predicate never becomes True."""
+
+    with asserts.assert_raises(UnexpectedBehaviorError):
+      expect_with_retry(
+          predicate=lambda: False, max_retries=3, retry_interval_sec=0
+      )
+
+  def test_retry_action_not_called_succeed(self):
+    """Test that the retry_action is not called if the predicate returns true in the first try."""
+    retry_action_called = False
+
+    def retry_action():
+      nonlocal retry_action_called
+      retry_action_called = True
+
+    expect_with_retry(
+        predicate=lambda: True,
+        retry_action=retry_action,
+        max_retries=5,
+        retry_interval_sec=0,
+    )
+    asserts.assert_false(
+        retry_action_called, "retry_action called."
+    )  # Assert retry_action was NOT called
+
+  def test_retry_action_not_called_failed(self):
+    """Test that the retry_action is not called if the max_retries is reached."""
+    retry_action_called = False
+
+    def retry_action():
+      nonlocal retry_action_called
+      retry_action_called = True
+
+    with asserts.assert_raises(UnexpectedBehaviorError):
+      expect_with_retry(
+          predicate=lambda: False,
+          retry_action=retry_action,
+          max_retries=1,
+          retry_interval_sec=0,
+      )
+    asserts.assert_false(
+        retry_action_called, "retry_action called."
+    )  # Assert retry_action was NOT called
+
+  def test_retry_action_called(self):
+    """Test that the retry_action is executed when provided."""
+    retry_action_called = False
+
+    def retry_action():
+      nonlocal retry_action_called
+      retry_action_called = True
+
+    with asserts.assert_raises(UnexpectedBehaviorError):
+      expect_with_retry(
+          predicate=lambda: False,
+          retry_action=retry_action,
+          max_retries=2,
+          retry_interval_sec=0,
+      )
+    asserts.assert_true(retry_action_called, "retry_action not called.")
diff --git a/staticlibs/tests/unit/host/python/run_tests.py b/staticlibs/tests/unit/host/python/run_tests.py
new file mode 100644
index 0000000..fa6a310
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/run_tests.py
@@ -0,0 +1,35 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+"""Main entrypoint for all of unittest."""
+
+import sys
+from host.python.adb_utils_test import TestAdbUtils
+from host.python.apf_utils_test import TestApfUtils
+from host.python.assert_utils_test import TestAssertUtils
+from mobly import suite_runner
+
+
+if __name__ == "__main__":
+  # For MoblyBinaryHostTest, this entry point will be called twice:
+  # 1. List tests.
+  #   <mobly-par-file-name> -- --list_tests
+  # 2. Run tests.
+  #   <mobly-par-file-name> -- --config=<yaml-path> --device_serial=<device-serial> --log_path=<log-path>
+  # Strip the "--" since suite runner doesn't recognize it.
+  sys.argv.pop(1)
+  # TODO: make the tests can be executed without manually list classes.
+  suite_runner.run_suite(
+      [TestAssertUtils, TestAdbUtils, TestApfUtils], sys.argv
+  )
diff --git a/staticlibs/tests/unit/host/python/test_config.xml b/staticlibs/tests/unit/host/python/test_config.xml
new file mode 100644
index 0000000..d3b200a
--- /dev/null
+++ b/staticlibs/tests/unit/host/python/test_config.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for NetworkStaticLibHostPythonTests">
+    <target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
+        <option name="dep-module" value="absl-py" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest" >
+        <option name="mobly-par-file-name" value="NetworkStaticLibHostPythonTests" />
+        <option name="mobly-test-timeout" value="3m" />
+    </test>
+</configuration>
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
index e23f999..4ed3afd 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.net.module.util
 
+import android.util.SparseArray
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import com.android.testutils.assertThrows
@@ -179,4 +180,20 @@
             CollectionUtils.assoc(listOf(1, 2), list15)
         }
     }
+
+    @Test
+    fun testGetIndexForValue() {
+        val sparseArray = SparseArray<String>();
+        sparseArray.put(5, "hello");
+        sparseArray.put(10, "abcd");
+        sparseArray.put(20, null);
+
+        val value1 = "abcd";
+        val value1Copy = String(value1.toCharArray())
+        val value2 = null;
+
+        assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1));
+        assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy));
+        assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2));
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index f32337d..9fb61d9 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -17,6 +17,7 @@
 package com.android.net.module.util;
 
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.provider.DeviceConfig.NAMESPACE_CAPTIVEPORTALLOGIN;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
@@ -233,8 +234,12 @@
                 TEST_EXPERIMENT_FLAG));
         doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(
+                NAMESPACE_CAPTIVEPORTALLOGIN, TEST_EXPERIMENT_FLAG));
         assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
     }
     @Test
     public void testIsFeatureEnabledFeatureDefaultDisabled() throws Exception {
@@ -242,8 +247,12 @@
                 TEST_EXPERIMENT_FLAG));
         doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
+        doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+                TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
 
         // If the flag is unset, package info is not queried
         verify(mContext, never()).getPackageManager();
@@ -257,8 +266,12 @@
                 TEST_EXPERIMENT_FLAG));
         doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+                TEST_EXPERIMENT_FLAG));
         assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
 
         // If the feature is force enabled, package info is not queried
         verify(mContext, never()).getPackageManager();
@@ -272,8 +285,12 @@
                 TEST_EXPERIMENT_FLAG));
         doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
+        doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+                TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
 
         // If the feature is force disabled, package info is not queried
         verify(mContext, never()).getPackageManager();
@@ -290,24 +307,36 @@
                 TEST_EXPERIMENT_FLAG));
         doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
+        doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+                TEST_EXPERIMENT_FLAG));
         assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
 
         // Feature should be disabled by flag value "999999999".
         doReturn("999999999").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
                 TEST_EXPERIMENT_FLAG));
         doReturn("999999999").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
+        doReturn("999999999").when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+                TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
 
         // If the flag is not set feature is disabled
         doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
                 TEST_EXPERIMENT_FLAG));
         doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
+        doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+                TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
     }
 
     @Test
@@ -320,9 +349,13 @@
                 NAMESPACE_CONNECTIVITY, TEST_EXPERIMENT_FLAG));
         doReturn("0").when(() -> DeviceConfig.getProperty(
                 NAMESPACE_TETHERING, TEST_EXPERIMENT_FLAG));
+        doReturn("0").when(() -> DeviceConfig.getProperty(
+                NAMESPACE_CAPTIVEPORTALLOGIN, TEST_EXPERIMENT_FLAG));
 
         assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+        assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
 
         doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
@@ -343,6 +376,21 @@
     }
 
     @Test
+    public void testIsCaptivePortalLoginFeatureEnabledCaching() throws Exception {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(
+                NAMESPACE_CAPTIVEPORTALLOGIN, TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
+        assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+                TEST_EXPERIMENT_FLAG));
+
+        // Package info is only queried once
+        verify(mContext, times(1)).getPackageManager();
+        verify(mContext, times(1)).getPackageName();
+        verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+    }
+
+    @Test
     public void testIsTetheringFeatureEnabledCaching() throws Exception {
         doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
                 TEST_EXPERIMENT_FLAG));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
new file mode 100644
index 0000000..7b1f08a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsUtilsTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import com.android.net.module.util.DnsUtils.equalsDnsLabelIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.equalsIgnoreDnsCase
+import com.android.net.module.util.DnsUtils.toDnsLabelsUpperCase
+import com.android.net.module.util.DnsUtils.toDnsUpperCase
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+class DnsUtilsTest {
+    @Test
+    fun testToDnsUpperCase() {
+        assertEquals("TEST", toDnsUpperCase("TEST"))
+        assertEquals("TEST", toDnsUpperCase("TeSt"))
+        assertEquals("TEST", toDnsUpperCase("test"))
+        assertEquals("TÉST", toDnsUpperCase("TÉST"))
+        assertEquals("ţéST", toDnsUpperCase("ţést"))
+        // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
+        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+        assertEquals(
+            "TEST: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+                toDnsUpperCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
+        )
+        // Also test some characters where the first surrogate is not \ud800
+        assertEquals(
+            "TEST: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+                toDnsUpperCase(
+                    "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+                )
+        )
+    }
+
+    @Test
+    fun testToDnsLabelsUpperCase() {
+        assertArrayEquals(
+            arrayOf("TEST", "TÉST", "ţéST"),
+            toDnsLabelsUpperCase(arrayOf("TeSt", "TÉST", "ţést"))
+        )
+    }
+
+    @Test
+    fun testEqualsIgnoreDnsCase() {
+        assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
+        assertTrue(equalsIgnoreDnsCase("TEST", "test"))
+        assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
+        assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
+        assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
+        // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
+        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
+        assertTrue(equalsIgnoreDnsCase(
+                "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+                "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
+        ))
+        // Also test some characters where the first surrogate is not \ud800
+        assertTrue(equalsIgnoreDnsCase(
+                "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
+                "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+        ))
+    }
+
+    @Test
+    fun testEqualsLabelIgnoreDnsCase() {
+        assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
+        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
new file mode 100644
index 0000000..4b740e3
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/GrowingIntArrayTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GrowingIntArrayTest {
+    @Test
+    fun testAddAndGet() {
+        val array = GrowingIntArray(1)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        assertEquals(-1, array.get(0))
+        assertEquals(0, array.get(1))
+        assertEquals(2, array.get(2))
+        assertEquals(3, array.length())
+    }
+
+    @Test
+    fun testForEach() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        val actual = mutableListOf<Int>()
+        array.forEach { actual.add(it) }
+
+        val expected = listOf(-1, 0, 2)
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun testForEach_EmptyArray() {
+        val array = GrowingIntArray(10)
+        array.forEach {
+            fail("This should not be called")
+        }
+    }
+
+    @Test
+    fun testRemoveValues() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(0)
+        array.add(2)
+
+        array.removeValues { it <= 0 }
+        assertEquals(1, array.length())
+        assertEquals(2, array.get(0))
+    }
+
+    @Test
+    fun testContains() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+
+        assertTrue(array.contains(-1))
+        assertTrue(array.contains(2))
+
+        assertFalse(array.contains(0))
+        assertFalse(array.contains(3))
+    }
+
+    @Test
+    fun testClear() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+        array.clear()
+
+        assertEquals(0, array.length())
+    }
+
+    @Test
+    fun testEnsureHasCapacity() {
+        val array = GrowingIntArray(0)
+        array.add(42)
+        array.ensureHasCapacity(2)
+
+        assertEquals(3, array.backingArrayLength)
+    }
+
+    @Test
+    fun testGetMinimizedBackingArray() {
+        val array = GrowingIntArray(10)
+        array.add(-1)
+        array.add(2)
+
+        assertContentEquals(intArrayOf(-1, 2), array.minimizedBackingArray)
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals("[]", GrowingIntArray(10).toString())
+        assertEquals("[1,2,3]", GrowingIntArray(3).apply {
+            add(1)
+            add(2)
+            add(3)
+        }.toString())
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
index 2785ea9..baadad0 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -17,13 +17,13 @@
 package com.android.net.module.util
 
 import android.net.NetworkStats
-import android.text.TextUtils
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import org.junit.Test
-import org.junit.runner.RunWith
+import com.android.testutils.assertEntryEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 
@@ -90,8 +90,6 @@
             NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12,
             0 /* operations */)
 
-        // TODO: Use assertEquals once all downstreams accept null iface in
-        // NetworkStats.Entry#equals.
         assertEntryEquals(expectedEntry, entry)
     }
 
@@ -121,22 +119,4 @@
         doReturn(txPackets).`when`(ret).getTxPackets()
         return ret
     }
-
-    /**
-     * Assert that the two {@link NetworkStats.Entry} are equals.
-     */
-    private fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
-        TextUtils.equals(left.iface, right.iface)
-        assertEquals(left.uid, right.uid)
-        assertEquals(left.set, right.set)
-        assertEquals(left.tag, right.tag)
-        assertEquals(left.metered, right.metered)
-        assertEquals(left.roaming, right.roaming)
-        assertEquals(left.defaultNetwork, right.defaultNetwork)
-        assertEquals(left.rxBytes, right.rxBytes)
-        assertEquals(left.rxPackets, right.rxPackets)
-        assertEquals(left.txBytes, right.txBytes)
-        assertEquals(left.txPackets, right.txPackets)
-        assertEquals(left.operations, right.operations)
-    }
 }
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
index e40cd6b..886336c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PacketBuilderTest.java
@@ -21,9 +21,13 @@
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_FRAGMENT_ID_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_FRAGMENT_ID_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
 import static com.android.net.module.util.NetworkStackConstants.TCP_HEADER_MIN_LEN;
 import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
@@ -54,6 +58,8 @@
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -489,10 +495,103 @@
                 (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef
             };
 
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_NO_FRAG =
+            new byte[] {
+                // packet = Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff", type='IPv6')/
+                //          IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80, fl=0x515ca,
+                //          hlim=0x40)/UDP(sport=9876, dport=433)/
+                //          Raw([i%256 for i in range(0, 500)]);
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x86, (byte) 0xdd,
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x01, (byte) 0xfc, (byte) 0x11, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x01, (byte) 0xfc, (byte) 0xd3, (byte) 0x9e,
+                // Data
+                // 500 bytes of repeated 0x00~0xff
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG1 =
+            new byte[] {
+                // packet = Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff", type='IPv6')/
+                //          IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80, fl=0x515ca,
+                //          hlim=0x40)/UDP(sport=9876, dport=433)/
+                //          Raw([i%256 for i in range(0, 500)]);
+                // packets=fragment6(packet, 400);
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x86, (byte) 0xdd,
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x01, (byte) 0x58, (byte) 0x2c, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // Fragement Header
+                (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                // UDP header
+                (byte) 0x26, (byte) 0x94, (byte) 0x01, (byte) 0xb1,
+                (byte) 0x01, (byte) 0xfc, (byte) 0xd3, (byte) 0x9e,
+                // Data
+                // 328 bytes of repeated 0x00~0xff, start:0x00 end:0x47
+            };
+
+    private static final byte[] TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG2 =
+            new byte[] {
+                // packet = Ether(src="11:22:33:44:55:66", dst="aa:bb:cc:dd:ee:ff", type='IPv6')/
+                //          IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80, fl=0x515ca,
+                //          hlim=0x40)/UDP(sport=9876, dport=433)/
+                //          Raw([i%256 for i in range(0, 500)]);
+                // packets=fragment6(packet, 400);
+                // Ether header
+                (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
+                (byte) 0xee, (byte) 0xff, (byte) 0x11, (byte) 0x22,
+                (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
+                (byte) 0x86, (byte) 0xdd,
+                // IPv6 header
+                (byte) 0x68, (byte) 0x05, (byte) 0x15, (byte) 0xca,
+                (byte) 0x00, (byte) 0xb4, (byte) 0x2c, (byte) 0x40,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
+                (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02,
+                // Fragement Header
+                (byte) 0x11, (byte) 0x00, (byte) 0x01, (byte) 0x50,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                // Data
+                // 172 bytes of repeated 0x00~0xff, start:0x48 end:0xf3
+            };
+
     /**
      * Build a packet which has ether header, IP header, TCP/UDP header and data.
      * The ethernet header and data are optional. Note that both source mac address and
-     * destination mac address are required for ethernet header.
+     * destination mac address are required for ethernet header. The packet will be fragmented into
+     * multiple smaller packets if the packet size exceeds L2 mtu.
      *
      * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      * |                Layer 2 header (EthernetHeader)                | (optional)
@@ -511,11 +610,12 @@
      * @param l4proto the layer 4 protocol. Only {@code IPPROTO_TCP} and {@code IPPROTO_UDP}
      *        currently supported.
      * @param payload the payload.
+     * @param l2mtu the Link MTU. It's the upper bound of each packet size. Zero means no limit.
      */
     @NonNull
-    private ByteBuffer buildPacket(@Nullable final MacAddress srcMac,
+    private List<ByteBuffer> buildPackets(@Nullable final MacAddress srcMac,
             @Nullable final MacAddress dstMac, final int l3proto, final int l4proto,
-            @Nullable final ByteBuffer payload)
+            @Nullable final ByteBuffer payload, int l2mtu)
             throws Exception {
         if (l3proto != IPPROTO_IP && l3proto != IPPROTO_IPV6) {
             fail("Unsupported layer 3 protocol " + l3proto);
@@ -562,7 +662,15 @@
             payload.clear();
         }
 
-        return packetBuilder.finalizePacket();
+        return packetBuilder.finalizePacket(l2mtu > 0 ? l2mtu : Integer.MAX_VALUE);
+    }
+
+    @NonNull
+    private ByteBuffer buildPacket(@Nullable final MacAddress srcMac,
+            @Nullable final MacAddress dstMac, final int l3proto, final int l4proto,
+            @Nullable final ByteBuffer payload)
+            throws Exception {
+        return buildPackets(srcMac, dstMac, l3proto, l4proto, payload, 0).get(0);
     }
 
     /**
@@ -874,6 +982,66 @@
         assertArrayEquals(TEST_PACKET_IPV6HDR_UDPHDR_DATA, packet.array());
     }
 
+    private void checkIpv6PacketIgnoreFragmentId(byte[] expected, byte[] actual) {
+        final int offset = ETHER_HEADER_LEN + IPV6_HEADER_LEN + IPV6_FRAGMENT_ID_OFFSET;
+        assertArrayEquals(Arrays.copyOf(expected, offset), Arrays.copyOf(actual, offset));
+        assertArrayEquals(
+                Arrays.copyOfRange(expected, offset + IPV6_FRAGMENT_ID_LEN, expected.length),
+                Arrays.copyOfRange(actual, offset + IPV6_FRAGMENT_ID_LEN, actual.length));
+    }
+
+    @Test
+    public void testBuildPacketIPv6FragmentUdpData() throws Exception {
+        // A UDP packet with 500 bytes payload will be fragmented into two UDP packets each carrying
+        // 328 and 172 bytes of payload if the Link MTU is 400. Note that only the first packet
+        // contains the original UDP header.
+        final int payloadLen = 500;
+        final int payloadLen1 = 328;
+        final int payloadLen2 = 172;
+        final int l2mtu = 400;
+        final byte[] payload = new byte[payloadLen];
+        // Initialize the payload with repeated values from 0x00 to 0xff.
+        for (int i = 0; i < payload.length; i++) {
+            payload[i] = (byte) (i & 0xff);
+        }
+
+        // Verify original UDP packet.
+        final ByteBuffer packet = buildPacket(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_UDP,
+                ByteBuffer.wrap(payload));
+        final int headerLen = TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_NO_FRAG.length;
+        assertArrayEquals(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_NO_FRAG,
+                Arrays.copyOf(packet.array(), headerLen));
+        assertArrayEquals(payload,
+                Arrays.copyOfRange(packet.array(), headerLen, headerLen + payloadLen));
+
+        // Verify fragments of UDP packet.
+        final List<ByteBuffer> packets = buildPackets(SRC_MAC, DST_MAC, IPPROTO_IPV6, IPPROTO_UDP,
+                ByteBuffer.wrap(payload), l2mtu);
+        assertEquals(2, packets.size());
+
+        // Verify first fragment.
+        int headerLen1 = TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG1.length;
+        // (1) Compare packet content up to the UDP header, excluding the fragment ID as it's a
+        // random value.
+        checkIpv6PacketIgnoreFragmentId(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG1,
+                Arrays.copyOf(packets.get(0).array(), headerLen1));
+        // (2) Compare UDP payload.
+        assertArrayEquals(Arrays.copyOf(payload, payloadLen1),
+                Arrays.copyOfRange(packets.get(0).array(), headerLen1, headerLen1 + payloadLen1));
+
+        // Verify second fragment (similar to the first one).
+        int headerLen2 = TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG2.length;
+        checkIpv6PacketIgnoreFragmentId(TEST_PACKET_ETHERHDR_IPV6HDR_UDPHDR_DATA_FRAG2,
+                Arrays.copyOf(packets.get(1).array(), headerLen2));
+        assertArrayEquals(Arrays.copyOfRange(payload, payloadLen1, payloadLen1 + payloadLen2),
+                Arrays.copyOfRange(packets.get(1).array(), headerLen2, headerLen2 + payloadLen2));
+        // Verify that the fragment IDs in the first and second fragments are the same.
+        final int offset = ETHER_HEADER_LEN + IPV6_HEADER_LEN + IPV6_FRAGMENT_ID_OFFSET;
+        assertArrayEquals(
+                Arrays.copyOfRange(packets.get(0).array(), offset, offset + IPV6_FRAGMENT_ID_LEN),
+                Arrays.copyOfRange(packets.get(1).array(), offset, offset + IPV6_FRAGMENT_ID_LEN));
+    }
+
     @Test
     public void testFinalizePacketWithoutIpv4Header() throws Exception {
         final ByteBuffer buffer = PacketBuilder.allocate(false /* hasEther */, IPPROTO_IP,
diff --git a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
similarity index 97%
rename from tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
index 4e15d5f..b04561c 100644
--- a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RoutingCoordinatorServiceTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.connectivity
+package com.android.net.module.util
 
 import android.net.INetd
 import android.os.Build
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
similarity index 98%
rename from Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
index 3a57fdd..d534054 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
@@ -13,13 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.networkstack.tethering.util
+package com.android.net.module.util
 
 import android.os.Message
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.util.State
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
+import com.android.net.module.util.SyncStateMachine.StateInfo
 import java.util.ArrayDeque
 import java.util.ArrayList
 import kotlin.test.assertFailsWith
@@ -45,7 +45,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
-class SynStateMachineTest {
+class SyncStateMachineTest {
     private val mState1 = spy(object : TestState(MSG_1) {})
     private val mState2 = spy(object : TestState(MSG_2) {})
     private val mState3 = spy(object : TestState(MSG_3) {})
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
index 4fc5ec2..d1df3a6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
@@ -252,8 +252,10 @@
     public void testToString() {
         NduseroptMessage msg = parseNduseroptMessage(toBuffer(MSG_PREF64));
         assertNotNull(msg);
-        assertEquals("Nduseroptmsg(10, 16, 1431655765, 134, 0, fe80:2:3:4:5:6:7:8%1431655765)",
-                msg.toString());
+        final String expected = "Nduseroptmsg(family:10, opts_len:16, ifindex:1431655765, "
+                + "icmp_type:134, icmp_code:0, srcaddr: fe80:2:3:4:5:6:7:8%1431655765, "
+                + "NdOptPref64(2001:db8:3:4:5:6::/96, 10064))";
+        assertEquals(expected, msg.toString());
     }
 
     // Convenience method to parse a NduseroptMessage that's not part of a netlink message.
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java
index 143e4d4..e42c552 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkConstantsTest.java
@@ -46,6 +46,7 @@
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWADDR;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNDUSEROPT;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWPREFIX;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWROUTE;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWRULE;
@@ -89,6 +90,7 @@
         assertEquals("RTM_NEWRULE", stringForNlMsgType(RTM_NEWRULE, NETLINK_ROUTE));
         assertEquals("RTM_DELRULE", stringForNlMsgType(RTM_DELRULE, NETLINK_ROUTE));
         assertEquals("RTM_GETRULE", stringForNlMsgType(RTM_GETRULE, NETLINK_ROUTE));
+        assertEquals("RTM_NEWPREFIX", stringForNlMsgType(RTM_NEWPREFIX, NETLINK_ROUTE));
         assertEquals("RTM_NEWNDUSEROPT", stringForNlMsgType(RTM_NEWNDUSEROPT, NETLINK_ROUTE));
 
         assertEquals("SOCK_DIAG_BY_FAMILY",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPioTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPioTest.java
new file mode 100644
index 0000000..0d88829
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPioTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.net.IpPrefix;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.PrefixInformationOption;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNdOptPioTest {
+    private static final IpPrefix TEST_PREFIX = new IpPrefix("2a00:79e1:abc:f605::/64");
+    private static final byte TEST_PIO_FLAGS_P_UNSET = (byte) 0xC0; // L=1,A=1
+    private static final byte TEST_PIO_FLAGS_P_SET   = (byte) 0xD0; // L=1,A=1,P=1
+    private static final String PIO_BYTES =
+            "0304"                                // type=3, length=4
+            + "40"                                // prefix length=64
+            + "C0"                                // L=1,A=1
+            + "00278D00"                          // valid=259200
+            + "00093A80"                          // preferred=604800
+            + "00000000"                          // Reserved2
+            + "2A0079E10ABCF6050000000000000000"; // prefix=2a00:79e1:abc:f605::
+
+    private static final String PIO_WITH_P_FLAG_BYTES =
+            "0304"                                // type=3, length=4
+            + "40"                                // prefix length=64
+            + "D0"                                // L=1,A=1,P=1
+            + "00278D00"                          // valid=2592000
+            + "00093A80"                          // preferred=604800
+            + "00000000"                          // Reserved2
+            + "2A0079E10ABCF6050000000000000000"; // prefix=2a00:79e1:abc:f605::
+
+    private static final String PIO_WITH_P_FLAG_INFINITY_LIFETIME_BYTES =
+            "0304"                                // type=3, length=4
+            + "40"                                // prefix length=64
+            + "D0"                                // L=1,A=1,P=1
+            + "FFFFFFFF"                          // valid=infinity
+            + "FFFFFFFF"                          // preferred=infintiy
+            + "00000000"                          // Reserved2
+            + "2A0079E10ABCF6050000000000000000"; // prefix=2a00:79e1:abc:f605::
+
+    private static void assertPioOptMatches(final StructNdOptPio opt, int length, byte flags,
+            long preferred, long valid, final IpPrefix prefix) {
+        assertEquals(StructNdOptPio.TYPE, opt.type);
+        assertEquals(length, opt.length);
+        assertEquals(flags, opt.flags);
+        assertEquals(preferred, opt.preferred);
+        assertEquals(valid, opt.valid);
+        assertEquals(prefix, opt.prefix);
+    }
+
+    private static void assertToByteBufferMatches(final StructNdOptPio opt, final String expected) {
+        String actual = HexEncoding.encodeToString(opt.toByteBuffer().array());
+        assertEquals(expected, actual);
+    }
+
+    private static void doPioParsingTest(final String optionHexString, int length, byte flags,
+            long preferred, long valid, final IpPrefix prefix) {
+        final byte[] rawBytes = HexEncoding.decode(optionHexString);
+        final StructNdOptPio opt = StructNdOptPio.parse(ByteBuffer.wrap(rawBytes));
+        assertPioOptMatches(opt, length, flags, preferred, valid, prefix);
+        assertToByteBufferMatches(opt, optionHexString);
+    }
+
+    @Test
+    public void testParsingPioWithoutPFlag() {
+        doPioParsingTest(PIO_BYTES, 4 /* length */, TEST_PIO_FLAGS_P_UNSET,
+                604800 /* preferred */, 2592000 /* valid */, TEST_PREFIX);
+    }
+
+    @Test
+    public void testParsingPioWithPFlag() {
+        doPioParsingTest(PIO_WITH_P_FLAG_BYTES, 4 /* length */, TEST_PIO_FLAGS_P_SET,
+                604800 /* preferred */, 2592000 /* valid */, TEST_PREFIX);
+    }
+
+    @Test
+    public void testParsingPioWithPFlag_infinityLifetime() {
+        doPioParsingTest(PIO_WITH_P_FLAG_INFINITY_LIFETIME_BYTES, 4 /* length */,
+                TEST_PIO_FLAGS_P_SET,
+                Integer.toUnsignedLong(INFINITE_LEASE) /* preferred */,
+                Integer.toUnsignedLong(INFINITE_LEASE) /* valid */,
+                TEST_PREFIX);
+    }
+
+    @Test
+    public void testToByteBuffer() {
+        final StructNdOptPio pio =
+                new StructNdOptPio(TEST_PIO_FLAGS_P_UNSET, 604800 /* preferred */,
+                        2592000 /* valid */, TEST_PREFIX);
+        assertToByteBufferMatches(pio, PIO_BYTES);
+    }
+
+    @Test
+    public void testToByteBuffer_withPFlag() {
+        final StructNdOptPio pio =
+                new StructNdOptPio(TEST_PIO_FLAGS_P_SET, 604800 /* preferred */,
+                        2592000 /* valid */, TEST_PREFIX);
+        assertToByteBufferMatches(pio, PIO_WITH_P_FLAG_BYTES);
+    }
+
+    @Test
+    public void testToByteBuffer_infinityLifetime() {
+        final StructNdOptPio pio =
+                new StructNdOptPio(TEST_PIO_FLAGS_P_SET,
+                        Integer.toUnsignedLong(INFINITE_LEASE) /* preferred */,
+                        Integer.toUnsignedLong(INFINITE_LEASE) /* valid */, TEST_PREFIX);
+        assertToByteBufferMatches(pio, PIO_WITH_P_FLAG_INFINITY_LIFETIME_BYTES);
+    }
+
+    private static ByteBuffer makePioOption(byte type, byte length, byte prefixLen, byte flags,
+            long valid, long preferred, final byte[] prefix) {
+        final PrefixInformationOption pio = new PrefixInformationOption(type, length, prefixLen,
+                flags, valid, preferred, 0 /* reserved */, prefix);
+        return ByteBuffer.wrap(pio.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+
+    @Test
+    public void testParsing_invalidOptionType() {
+        final ByteBuffer buf = makePioOption((byte) 24 /* wrong type:RIO */,
+                (byte) 4 /* length */, (byte) 64 /* prefixLen */, TEST_PIO_FLAGS_P_SET,
+                2592000 /* valid */, 604800 /* preferred */, TEST_PREFIX.getRawAddress());
+        assertNull(StructNdOptPio.parse(buf));
+    }
+
+    @Test
+    public void testParsing_invalidOptionLength() {
+        final ByteBuffer buf = makePioOption((byte) 24 /* wrong type:RIO */,
+                (byte) 3 /* wrong length */, (byte) 64 /* prefixLen */,
+                TEST_PIO_FLAGS_P_SET, 2592000 /* valid */, 604800 /* preferred */,
+                TEST_PREFIX.getRawAddress());
+        assertNull(StructNdOptPio.parse(buf));
+    }
+
+    @Test
+    public void testParsing_truncatedByteBuffer() {
+        final ByteBuffer buf = makePioOption((byte) 3 /* type */, (byte) 4 /* length */,
+                (byte) 64 /* prefixLen */, TEST_PIO_FLAGS_P_SET,
+                2592000 /* valid */, 604800 /* preferred */, TEST_PREFIX.getRawAddress());
+        final int len = buf.limit();
+        for (int i = 0; i < buf.limit() - 1; i++) {
+            buf.flip();
+            buf.limit(i);
+            assertNull("Option truncated to " + i + " bytes, should have returned null",
+                    StructNdOptPio.parse(buf));
+        }
+        buf.flip();
+        buf.limit(len);
+
+        final StructNdOptPio opt = StructNdOptPio.parse(buf);
+        assertPioOptMatches(opt, (byte) 4 /* length */, TEST_PIO_FLAGS_P_SET,
+                604800 /* preferred */, 2592000 /* valid */, TEST_PREFIX);
+    }
+
+    @Test
+    public void testParsing_invalidByteBufferLength() {
+        final ByteBuffer buf = makePioOption((byte) 3 /* type */, (byte) 4 /* length */,
+                (byte) 64 /* prefixLen */, TEST_PIO_FLAGS_P_SET,
+                2592000 /* valid */, 604800 /* preferred */, TEST_PREFIX.getRawAddress());
+        buf.limit(31); // less than 4 * 8
+        assertNull(StructNdOptPio.parse(buf));
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer buf = makePioOption((byte) 3 /* type */, (byte) 4 /* length */,
+                (byte) 64 /* prefixLen */, TEST_PIO_FLAGS_P_SET,
+                2592000 /* valid */, 604800 /* preferred */, TEST_PREFIX.getRawAddress());
+        final StructNdOptPio opt = StructNdOptPio.parse(buf);
+        final String expected = "NdOptPio"
+                + "(flags:D0, preferred lft:604800, valid lft:2592000,"
+                + " prefix:2a00:79e1:abc:f605::/64)";
+        assertEquals(expected, opt.toString());
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
index 4c3fde6..b5e3dff 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -69,11 +69,11 @@
     }
 
     @Test
-    public void testGetValueAsIntger() {
+    public void testGetValueAsInteger() {
         final StructNlAttr attr1 = new StructNlAttr(IFA_FLAGS, TEST_ADDR_FLAGS);
         final Integer integer1 = attr1.getValueAsInteger();
         final int int1 = attr1.getValueAsInt(0x08 /* default value */);
-        assertEquals(integer1, new Integer(TEST_ADDR_FLAGS));
+        assertEquals(integer1, Integer.valueOf(TEST_ADDR_FLAGS));
         assertEquals(int1, TEST_ADDR_FLAGS);
 
         // Malformed attribute.
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/FragmentHeaderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/FragmentHeaderTest.java
new file mode 100644
index 0000000..1a78ca5
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/FragmentHeaderTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class FragmentHeaderTest {
+    private static final byte[] HEADER_BYTES = new byte[] {
+        17, /* nextHeader  */
+        0, /* reserved */
+        15, 1, /* fragmentOffset */
+        1, 2, 3, 4 /* identification */
+    };
+
+    @Test
+    public void testConstructor() {
+        FragmentHeader fragHdr = new FragmentHeader((short) 10 /* nextHeader */,
+                (byte) 11 /* reserved */,
+                12 /* fragmentOffset */,
+                13 /* identification */);
+
+        assertEquals(10, fragHdr.nextHeader);
+        assertEquals(11, fragHdr.reserved);
+        assertEquals(12, fragHdr.fragmentOffset);
+        assertEquals(13, fragHdr.identification);
+    }
+
+    @Test
+    public void testParseFragmentHeader() {
+        final ByteBuffer buf = ByteBuffer.wrap(HEADER_BYTES);
+        buf.order(ByteOrder.BIG_ENDIAN);
+        FragmentHeader fragHdr = FragmentHeader.parse(FragmentHeader.class, buf);
+
+        assertEquals(17, fragHdr.nextHeader);
+        assertEquals(0, fragHdr.reserved);
+        assertEquals(0xF01, fragHdr.fragmentOffset);
+        assertEquals(0x1020304, fragHdr.identification);
+    }
+
+    @Test
+    public void testWriteToBytes() {
+        FragmentHeader fragHdr = new FragmentHeader((short) 17 /* nextHeader */,
+                (byte) 0 /* reserved */,
+                0xF01 /* fragmentOffset */,
+                0x1020304 /* identification */);
+
+        byte[] bytes = fragHdr.writeToBytes(ByteOrder.BIG_ENDIAN);
+
+        assertArrayEquals("bytes = " + Arrays.toString(bytes), HEADER_BYTES, bytes);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt b/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt
new file mode 100644
index 0000000..7e508fb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/DefaultNetworkRestoreMonitorTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.notification.RunListener
+import org.junit.runner.notification.RunNotifier
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class DefaultNetworkRestoreMonitorTest {
+    private val restoreDefaultNetworkDesc =
+            Description.createSuiteDescription("RestoreDefaultNetwork")
+    private val testDesc = Description.createTestDescription("testClass", "testMethod")
+    private val wifiCap = NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_WIFI)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+            .build()
+    private val cellCap = NetworkCapabilities.Builder()
+            .addTransportType(TRANSPORT_CELLULAR)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+            .build()
+    private val cm = mock(ConnectivityManager::class.java)
+    private val pm = mock(PackageManager::class.java).also {
+        doReturn(true).`when`(it).hasSystemFeature(anyString())
+    }
+    private val ctx = mock(Context::class.java).also {
+        doReturn(cm).`when`(it).getSystemService(ConnectivityManager::class.java)
+        doReturn(pm).`when`(it).getPackageManager()
+    }
+    private val notifier = mock(RunNotifier::class.java)
+    private val defaultNetworkMonitor = DefaultNetworkRestoreMonitor(
+        ctx,
+        notifier,
+        timeoutMs = 0
+    )
+
+    private fun getRunListener(): RunListener {
+        val captor = ArgumentCaptor.forClass(RunListener::class.java)
+        verify(notifier).addListener(captor.capture())
+        return captor.value
+    }
+
+    private fun mockDefaultNetworkCapabilities(cap: NetworkCapabilities?) {
+        if (cap == null) {
+            doNothing().`when`(cm).registerDefaultNetworkCallback(any())
+            return
+        }
+        doAnswer {
+            val callback = it.getArgument(0) as NetworkCallback
+            callback.onCapabilitiesChanged(Network(100), cap)
+        }.`when`(cm).registerDefaultNetworkCallback(any())
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_defaultNetworkRestored() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier, never()).fireTestFailure(any())
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testStartWithoutDefaultNetwork() {
+        // There is no default network when the tests start
+        mockDefaultNetworkCapabilities(null)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        mockDefaultNetworkCapabilities(wifiCap)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called
+        inOrder.verify(notifier).fireTestFailure(any())
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testEndWithoutDefaultNetwork() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        // There is no default network after the test
+        mockDefaultNetworkCapabilities(null)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called with method name
+        inOrder.verify(
+                notifier
+        ).fireTestFailure(
+                argThat{failure -> failure.exception.message?.contains("testMethod") ?: false}
+        )
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+
+    @Test
+    fun testDefaultNetworkRestoreMonitor_testChangeDefaultNetwork() {
+        mockDefaultNetworkCapabilities(wifiCap)
+        defaultNetworkMonitor.init(mock(ConnectUtil::class.java))
+
+        // The default network transport types change after the test
+        mockDefaultNetworkCapabilities(cellCap)
+        val listener = getRunListener()
+        listener.testFinished(testDesc)
+
+        defaultNetworkMonitor.reportResultAndCleanUp(restoreDefaultNetworkDesc)
+        val inOrder = inOrder(notifier)
+        inOrder.verify(notifier).fireTestStarted(restoreDefaultNetworkDesc)
+        // fireTestFailure is called with method name
+        inOrder.verify(
+                notifier
+        ).fireTestFailure(
+                argThat{failure -> failure.exception.message?.contains("testMethod") ?: false}
+        )
+        inOrder.verify(notifier).fireTestFinished(restoreDefaultNetworkDesc)
+        inOrder.verify(notifier).removeListener(listener)
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
new file mode 100644
index 0000000..57920fc
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/NetworkStatsUtilsTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 android.net.NetworkStats.DEFAULT_NETWORK_NO
+import android.net.NetworkStats.METERED_NO
+import android.net.NetworkStats.ROAMING_NO
+import android.net.NetworkStats.SET_DEFAULT
+import android.net.NetworkStats.TAG_NONE
+import android.os.Build
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val TEST_IFACE = "test0"
+private val TEST_IFACE2: String? = null
+private const val TEST_START = 1194220800000L
+
+@RunWith(JUnit4::class)
+class NetworkStatsUtilsTest {
+    // This is a unit test for a test utility that uses R APIs
+    @Rule @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
+
+    @Test
+    fun testOrderInsensitiveEquals() {
+        val testEntry = arrayOf(
+                NetworkStats.Entry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 128L, 8L, 0L, 2L, 20L),
+                NetworkStats.Entry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L)
+        )
+
+        // Verify equals of empty stats regardless of initial capacity.
+        val red = NetworkStats(TEST_START, 0)
+        val blue = NetworkStats(TEST_START, 1)
+        assertTrue(orderInsensitiveEquals(red, blue))
+        assertTrue(orderInsensitiveEquals(blue, red))
+
+        // Verify not equal.
+        red.combineValues(testEntry[1])
+        blue.combineValues(testEntry[0]).combineValues(testEntry[1])
+        assertFalse(orderInsensitiveEquals(red, blue))
+        assertFalse(orderInsensitiveEquals(blue, red))
+
+        // Verify equals even if the order of entries are not the same.
+        red.combineValues(testEntry[0])
+        assertTrue(orderInsensitiveEquals(red, blue))
+        assertTrue(orderInsensitiveEquals(blue, red))
+    }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index a8e5a69..8c71a91 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -40,6 +40,7 @@
         "net-utils-device-common-async",
         "net-utils-device-common-netlink",
         "net-utils-device-common-struct",
+        "net-utils-device-common-struct-base",
         "net-utils-device-common-wear",
         "modules-utils-build_system",
     ],
@@ -85,8 +86,8 @@
 java_test_host {
     name: "net-tests-utils-host-common",
     srcs: [
-        "host/**/*.java",
-        "host/**/*.kt",
+        "host/java/**/*.java",
+        "host/java/**/*.kt",
     ],
     libs: ["tradefed"],
     test_suites: [
@@ -98,6 +99,16 @@
         "mcts-networking",
         "mts-tethering",
         "mcts-tethering",
+        "mcts-wifi",
+        "mcts-dnsresolver",
     ],
     data: [":ConnectivityTestPreparer"],
 }
+
+python_library_host {
+    name: "net-tests-utils-host-python-common",
+    srcs: [
+        "host/python/*.py",
+    ],
+    pkg_path: "net_tests_utils",
+}
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
index 5af8c14..394c6be 100644
--- a/staticlibs/testutils/app/connectivitychecker/Android.bp
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -30,7 +30,6 @@
         "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/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index df6067d..e634f0e 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -34,17 +34,13 @@
     private val connectUtil by lazy { ConnectUtil(context) }
 
     @Test
-    fun testCheckConnectivity() {
-        checkWifiSetup()
-        checkTelephonySetup()
-    }
-
-    private fun checkWifiSetup() {
+    fun testCheckWifiSetup() {
         if (!pm.hasSystemFeature(FEATURE_WIFI)) return
         connectUtil.ensureWifiValidated()
     }
 
-    private fun checkTelephonySetup() {
+    @Test
+    fun testCheckTelephonySetup() {
         if (!pm.hasSystemFeature(FEATURE_TELEPHONY)) return
         val tm = context.getSystemService(TelephonyManager::class.java)
                 ?: fail("Could not get telephony service")
@@ -52,7 +48,7 @@
         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"
+                ":ignore-mobile-data-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")
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
new file mode 100644
index 0000000..89de0b3
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoCloseTestInterfaceRule.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+class AutoCloseTestInterfaceRule(
+        private val context: Context,
+    ) : TestRule {
+    private val tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+        context.getSystemService(TestNetworkManager::class.java)!!
+    }
+    private val ifaces = ArrayList<TestNetworkInterface>()
+
+    fun createTapInterface(): TestNetworkInterface {
+        return runAsShell(MANAGE_TEST_NETWORKS) {
+            tnm.createTapInterface()
+        }.also {
+            ifaces.add(it)
+        }
+    }
+
+    private fun closeAllInterfaces() {
+        // TODO: wait on RTM_DELLINK before proceeding.
+        for (iface in ifaces) {
+            // ParcelFileDescriptor prevents the fd from being double closed.
+            iface.getFileDescriptor().close()
+        }
+    }
+
+    private inner class AutoCloseTestInterfaceRuleStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            tryTest {
+                base.evaluate()
+            } cleanup {
+                closeAllInterfaces()
+            }
+        }
+    }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return AutoCloseTestInterfaceRuleStatement(base, description)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
new file mode 100644
index 0000000..93422ad
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Handler
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.RecorderCallback.CallbackEntry
+import java.util.Collections
+import kotlin.test.fail
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * A rule to file [NetworkCallback]s to request or watch networks.
+ *
+ * The callbacks filed in test methods are automatically unregistered when the method completes.
+ */
+class AutoReleaseNetworkCallbackRule : NetworkCallbackHelper(), TestRule {
+    override fun apply(base: Statement, description: Description): Statement {
+        return RequestCellNetworkStatement(base, description)
+    }
+
+    private inner class RequestCellNetworkStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            tryTest {
+                base.evaluate()
+            } cleanup {
+                unregisterAll()
+            }
+        }
+    }
+}
+
+/**
+ * Helps file [NetworkCallback]s to request or watch networks, keeping track of them for cleanup.
+ */
+open class NetworkCallbackHelper {
+    private val cm by lazy {
+        InstrumentationRegistry.getInstrumentation().context
+            .getSystemService(ConnectivityManager::class.java)
+            ?: fail("ConnectivityManager not found")
+    }
+    private val cbToCleanup = Collections.synchronizedSet(mutableSetOf<NetworkCallback>())
+    private var cellRequestCb: TestableNetworkCallback? = null
+
+    /**
+     * Convenience method to request a cell network, similarly to [requestNetwork].
+     *
+     * The rule will keep tract of a single cell network request, which can be unrequested manually
+     * using [unrequestCell].
+     */
+    fun requestCell(): Network {
+        if (cellRequestCb != null) {
+            fail("Cell network was already requested")
+        }
+        val cb = requestNetwork(
+            NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .build()
+        )
+        cellRequestCb = cb
+        return cb.expect<CallbackEntry.Available>(
+            errorMsg = "Cell network not available. " +
+                    "Please ensure the device has working mobile data."
+        ).network
+    }
+
+    /**
+     * Unrequest a cell network requested through [requestCell].
+     */
+    fun unrequestCell() {
+        val cb = cellRequestCb ?: fail("Cell network was not requested")
+        unregisterNetworkCallback(cb)
+        cellRequestCb = null
+    }
+
+    private fun addCallback(
+        cb: TestableNetworkCallback,
+        registrar: (TestableNetworkCallback) -> Unit
+    ): TestableNetworkCallback {
+        registrar(cb)
+        cbToCleanup.add(cb)
+        return cb
+    }
+
+    /**
+     * File a request for a Network.
+     *
+     * This will fail tests (throw) if the cell network cannot be obtained, or if it was already
+     * requested.
+     *
+     * Tests may call [unregisterNetworkCallback] once they are done using the returned [Network],
+     * otherwise it will be automatically unrequested after the test.
+     */
+    @JvmOverloads
+    fun requestNetwork(
+        request: NetworkRequest,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler? = null
+    ) = addCallback(cb) {
+        if (handler == null) {
+            cm.requestNetwork(request, it)
+        } else {
+            cm.requestNetwork(request, it, handler)
+        }
+    }
+
+    /**
+     * Overload of [requestNetwork] that allows specifying a timeout.
+     */
+    @JvmOverloads
+    fun requestNetwork(
+        request: NetworkRequest,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        timeoutMs: Int,
+    ) = addCallback(cb) { cm.requestNetwork(request, it, timeoutMs) }
+
+    /**
+     * File a callback for a NetworkRequest.
+     *
+     * This will fail tests (throw) if the cell network cannot be obtained, or if it was already
+     * requested.
+     *
+     * Tests may call [unregisterNetworkCallback] once they are done using the returned [Network],
+     * otherwise it will be automatically unrequested after the test.
+     */
+    @JvmOverloads
+    fun registerNetworkCallback(
+        request: NetworkRequest,
+        cb: TestableNetworkCallback = TestableNetworkCallback()
+    ) = addCallback(cb) { cm.registerNetworkCallback(request, it) }
+
+    /**
+     * @see ConnectivityManager.registerDefaultNetworkCallback
+     */
+    @JvmOverloads
+    fun registerDefaultNetworkCallback(
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler? = null
+    ) = addCallback(cb) {
+        if (handler == null) {
+            cm.registerDefaultNetworkCallback(it)
+        } else {
+            cm.registerDefaultNetworkCallback(it, handler)
+        }
+    }
+
+    /**
+     * @see ConnectivityManager.registerSystemDefaultNetworkCallback
+     */
+    @JvmOverloads
+    fun registerSystemDefaultNetworkCallback(
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler
+    ) = addCallback(cb) { cm.registerSystemDefaultNetworkCallback(it, handler) }
+
+    /**
+     * @see ConnectivityManager.registerDefaultNetworkCallbackForUid
+     */
+    @JvmOverloads
+    fun registerDefaultNetworkCallbackForUid(
+        uid: Int,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler
+    ) = addCallback(cb) { cm.registerDefaultNetworkCallbackForUid(uid, it, handler) }
+
+    /**
+     * @see ConnectivityManager.registerBestMatchingNetworkCallback
+     */
+    @JvmOverloads
+    fun registerBestMatchingNetworkCallback(
+        request: NetworkRequest,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler
+    ) = addCallback(cb) { cm.registerBestMatchingNetworkCallback(request, it, handler) }
+
+    /**
+     * @see ConnectivityManager.requestBackgroundNetwork
+     */
+    @JvmOverloads
+    fun requestBackgroundNetwork(
+        request: NetworkRequest,
+        cb: TestableNetworkCallback = TestableNetworkCallback(),
+        handler: Handler
+    ) = addCallback(cb) { cm.requestBackgroundNetwork(request, it, handler) }
+
+    /**
+     * Unregister a callback filed using registration methods in this class.
+     */
+    fun unregisterNetworkCallback(cb: NetworkCallback) {
+        cm.unregisterNetworkCallback(cb)
+        cbToCleanup.remove(cb)
+    }
+
+    /**
+     * Unregister all callbacks that were filed using registration methods in this class.
+     */
+    fun unregisterAll() {
+        cbToCleanup.forEach { cm.unregisterNetworkCallback(it) }
+        cbToCleanup.clear()
+        cellRequestCb = null
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index 8090d5b..3857810 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -86,6 +86,7 @@
         val callback = TestableNetworkCallback(timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
         cm.registerNetworkCallback(NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
                 .build(), callback)
 
         return tryTest {
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
new file mode 100644
index 0000000..dd52d0b
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DefaultNetworkRestoreMonitor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.internal.annotations.VisibleForTesting
+import com.android.net.module.util.BitUtils
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.runner.Description
+import org.junit.runner.notification.Failure
+import org.junit.runner.notification.RunListener
+import org.junit.runner.notification.RunNotifier
+
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+class DefaultNetworkRestoreMonitor(
+        ctx: Context,
+        private val notifier: RunNotifier,
+        private val timeoutMs: Long = 30_000
+) {
+    var firstFailure: Exception? = null
+    var initialTransports = 0L
+    val cm = ctx.getSystemService(ConnectivityManager::class.java)!!
+    val pm = ctx.packageManager
+    val listener = object : RunListener() {
+        override fun testFinished(desc: Description) {
+            // Only the first method that does not restore the default network should be blamed.
+            if (firstFailure != null) {
+                return
+            }
+            val cb = TestableNetworkCallback()
+            cm.registerDefaultNetworkCallback(cb)
+            try {
+                cb.eventuallyExpect<RecorderCallback.CallbackEntry.CapabilitiesChanged>(
+                    timeoutMs = timeoutMs
+                ) {
+                    BitUtils.packBits(it.caps.transportTypes) == initialTransports &&
+                            it.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                }
+            } catch (e: AssertionError) {
+                firstFailure = IllegalStateException(desc.methodName + " does not restore the" +
+                        "default network, initialTransports = $initialTransports", e)
+            } finally {
+                cm.unregisterNetworkCallback(cb)
+            }
+        }
+    }
+
+    fun init(connectUtil: ConnectUtil) {
+        // Ensure Wi-Fi and cellular connection before running test to avoid starting test
+        // with unexpected default network.
+        // ConnectivityTestTargetPreparer does the same thing, but it's possible that previous tests
+        // don't enable DefaultNetworkRestoreMonitor and the default network is not restored.
+        // This can be removed if all tests enable DefaultNetworkRestoreMonitor
+        if (pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            connectUtil.ensureWifiValidated()
+        }
+        if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+            connectUtil.ensureCellularValidated()
+        }
+
+        val capFuture = CompletableFuture<NetworkCapabilities>()
+        val cb = object : ConnectivityManager.NetworkCallback() {
+            override fun onCapabilitiesChanged(
+                    network: Network,
+                    cap: NetworkCapabilities
+            ) {
+                capFuture.complete(cap)
+            }
+        }
+        cm.registerDefaultNetworkCallback(cb)
+        try {
+            val cap = capFuture.get(10_000, TimeUnit.MILLISECONDS)
+            initialTransports = BitUtils.packBits(cap.transportTypes)
+        } catch (e: Exception) {
+            firstFailure = IllegalStateException(
+                    "Failed to get default network status before starting tests", e
+            )
+        } finally {
+            cm.unregisterNetworkCallback(cb)
+        }
+        notifier.addListener(listener)
+    }
+
+    fun reportResultAndCleanUp(desc: Description) {
+        notifier.fireTestStarted(desc)
+        if (firstFailure != null) {
+            notifier.fireTestFailure(
+                    Failure(desc, firstFailure)
+            )
+        }
+        notifier.fireTestFinished(desc)
+        notifier.removeListener(listener)
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 69fdbf8..a014834 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -16,6 +16,8 @@
 
 package com.android.testutils
 
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
@@ -57,6 +59,10 @@
 class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
     private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
     private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
+    private val restoreDefaultNetworkDesc =
+            Description.createTestDescription(klass, "RestoreDefaultNetwork")
+    private val restoreDefaultNetwork = klass.isAnnotationPresent(RestoreDefaultNetwork::class.java)
+    val ctx = ApplicationProvider.getApplicationContext<Context>()
 
     // Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
     // Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
@@ -71,6 +77,10 @@
     // TODO(b/307693729): Remove this annotation and monitor thread leak by default.
     annotation class MonitorThreadLeak
 
+    // Annotation for test classes to indicate the test runner should verify the default network is
+    // restored after each test.
+    annotation class RestoreDefaultNetwork
+
     private val baseRunner: RunnerWrapper<*>? = klass.let {
         val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
         val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -90,25 +100,10 @@
         Modifier.isStatic(it.modifiers) &&
                 it.isAnnotationPresent(Parameterized.Parameters::class.java) }
 
-    override fun run(notifier: RunNotifier) {
-        if (baseRunner == null) {
-            // 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"))
-            return
-        }
-        if (!shouldThreadLeakFailTest) {
-            baseRunner.run(notifier)
-            return
-        }
-
-        // Dump threads as a baseline to monitor thread leaks.
-        val threadCountsBeforeTest = getAllThreadNameCounts()
-
-        baseRunner.run(notifier)
-
+    private fun checkThreadLeak(
+            notifier: RunNotifier,
+            threadCountsBeforeTest: Map<String, Int>
+    ) {
         notifier.fireTestStarted(leakMonitorDesc)
         val threadCountsAfterTest = getAllThreadNameCounts()
         // TODO : move CompareOrUpdateResult to its own util instead of LinkProperties.
@@ -122,13 +117,48 @@
         val increasedThreads = threadsDiff.updated
                 .filter { threadCountsBeforeTest[it.key]!! < it.value }
         if (threadsDiff.added.isNotEmpty() || increasedThreads.isNotEmpty()) {
-            notifier.fireTestFailure(Failure(leakMonitorDesc,
-                    IllegalStateException("Unexpected thread changes: $threadsDiff")))
+            notifier.fireTestFailure(Failure(
+                    leakMonitorDesc,
+                    IllegalStateException("Unexpected thread changes: $threadsDiff")
+            ))
         }
+        notifier.fireTestFinished(leakMonitorDesc)
+    }
+
+    override fun run(notifier: RunNotifier) {
+        if (baseRunner == null) {
+            // 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")
+            )
+            return
+        }
+
+        val networkRestoreMonitor = if (restoreDefaultNetwork) {
+            DefaultNetworkRestoreMonitor(ctx, notifier).apply{
+                init(ConnectUtil(ctx))
+            }
+        } else {
+            null
+        }
+        val threadCountsBeforeTest = if (shouldThreadLeakFailTest) {
+            // Dump threads as a baseline to monitor thread leaks.
+            getAllThreadNameCounts()
+        } else {
+            null
+        }
+
+        baseRunner.run(notifier)
+
+        if (threadCountsBeforeTest != null) {
+            checkThreadLeak(notifier, threadCountsBeforeTest)
+        }
+        networkRestoreMonitor?.reportResultAndCleanUp(restoreDefaultNetworkDesc)
         // Clears up internal state of all inline mocks.
         // TODO: Call clearInlineMocks() at the end of each test.
         Mockito.framework().clearInlineMocks()
-        notifier.fireTestFinished(leakMonitorDesc)
     }
 
     private fun getAllThreadNameCounts(): Map<String, Int> {
@@ -152,6 +182,9 @@
             if (shouldThreadLeakFailTest) {
                 it.addChild(leakMonitorDesc)
             }
+            if (restoreDefaultNetwork) {
+                it.addChild(restoreDefaultNetworkDesc)
+            }
         }
     }
 
@@ -162,7 +195,14 @@
         // When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
         if (baseRunner == null) return 1
 
-        return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
+        var testCount = baseRunner.testCount()
+        if (shouldThreadLeakFailTest) {
+            testCount += 1
+        }
+        if (restoreDefaultNetwork) {
+            testCount += 1
+        }
+        return testCount
     }
 
     @Throws(NoTestsRemainException::class)
diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
similarity index 97%
rename from tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
rename to staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
index 5ba6c4c..8b88224 100644
--- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.net.cts
+package com.android.testutils
 
 import android.net.DnsResolver
 import android.net.Network
@@ -287,6 +287,12 @@
 ): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isQueryFor(recordName, *requiredTypes) }
 
 fun TapPacketReader.pollForReply(
+    recordName: String,
+    type: Int,
+    timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isReplyFor(recordName, type) }
+
+fun TapPacketReader.pollForReply(
     serviceName: String,
     serviceType: String,
     timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
index 8324b25..f2b14f5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
@@ -17,7 +17,14 @@
 package com.android.testutils
 
 import android.net.NetworkStats
+import android.text.TextUtils
+import com.android.modules.utils.build.SdkLevel
 import kotlin.test.assertTrue
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito
 
 @JvmOverloads
 fun orderInsensitiveEquals(
@@ -26,7 +33,7 @@
     compareTime: Boolean = false
 ): Boolean {
     if (leftStats == rightStats) return true
-    if (compareTime && leftStats.getElapsedRealtime() != rightStats.getElapsedRealtime()) {
+    if (compareTime && leftStats.elapsedRealtime != rightStats.elapsedRealtime) {
         return false
     }
 
@@ -47,12 +54,41 @@
                 left.metered, left.roaming, left.defaultNetwork, i)
         if (j == -1) return false
         rightTrimmedEmpty.getValues(j, right)
-        if (left != right) return false
+        if (SdkLevel.isAtLeastT()) {
+            if (left != right) return false
+        } else {
+            if (!checkEntryEquals(left, right)) return false
+        }
     }
     return true
 }
 
 /**
+ * Assert that the two {@link NetworkStats.Entry} are equals.
+ */
+fun assertEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry) {
+    assertTrue(checkEntryEquals(left, right))
+}
+
+// TODO: Make all callers use NetworkStats.Entry#equals once S- downstreams
+//  are no longer supported. Because NetworkStats is mainlined on T+ and
+//  NetworkStats.Entry#equals in S- does not support null iface.
+fun checkEntryEquals(left: NetworkStats.Entry, right: NetworkStats.Entry): Boolean {
+    return TextUtils.equals(left.iface, right.iface) &&
+            left.uid == right.uid &&
+            left.set == right.set &&
+            left.tag == right.tag &&
+            left.metered == right.metered &&
+            left.roaming == right.roaming &&
+            left.defaultNetwork == right.defaultNetwork &&
+            left.rxBytes == right.rxBytes &&
+            left.rxPackets == right.rxPackets &&
+            left.txBytes == right.txBytes &&
+            left.txPackets == right.txPackets &&
+            left.operations == right.operations
+}
+
+/**
  * Assert that two {@link NetworkStats} are equals, assuming the order of the records are not
  * necessarily the same.
  *
@@ -66,7 +102,7 @@
     compareTime: Boolean = false
 ) {
     assertTrue(orderInsensitiveEquals(expected, actual, compareTime),
-            "expected: " + expected + " but was: " + actual)
+            "expected: $expected but was: $actual")
 }
 
 /**
@@ -74,5 +110,5 @@
  * object.
  */
 fun assertParcelingIsLossless(stats: NetworkStats) {
-    assertParcelingIsLossless(stats, { a, b -> orderInsensitiveEquals(a, b) })
+    assertParcelingIsLossless(stats) { a, b -> orderInsensitiveEquals(a, b) }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
new file mode 100644
index 0000000..d5e91c2
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/SetFeatureFlagsRule.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.com.android.testutils
+
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * A JUnit Rule that sets feature flags based on `@FeatureFlag` annotations.
+ *
+ * This rule enables dynamic control of feature flag states during testing.
+ * And restores the original values after performing tests.
+ *
+ * **Usage:**
+ * ```kotlin
+ * class MyTestClass {
+ *   @get:Rule
+ *   val setFeatureFlagsRule = SetFeatureFlagsRule(setFlagsMethod = (name, enabled) -> {
+ *     // Custom handling code.
+ *   }, (name) -> {
+ *     // Custom getter code to retrieve the original values.
+ *   })
+ *
+ *   // ... test methods with @FeatureFlag annotations
+ *   @FeatureFlag("FooBar1", true)
+ *   @FeatureFlag("FooBar2", false)
+ *   @Test
+ *   fun testFooBar() {}
+ * }
+ * ```
+ */
+class SetFeatureFlagsRule(
+    val setFlagsMethod: (name: String, enabled: Boolean?) -> Unit,
+                          val getFlagsMethod: (name: String) -> Boolean?
+) : TestRule {
+    /**
+     * This annotation marks a test method as requiring a specific feature flag to be configured.
+     *
+     * Use this on test methods to dynamically control feature flag states during testing.
+     *
+     * @param name The name of the feature flag.
+     * @param enabled The desired state (true for enabled, false for disabled) of the feature flag.
+     */
+    @Target(AnnotationTarget.FUNCTION)
+    @Retention(AnnotationRetention.RUNTIME)
+    annotation class FeatureFlag(val name: String, val enabled: Boolean = true)
+
+    /**
+     * This method is the core of the rule, executed by the JUnit framework before each test method.
+     *
+     * It retrieves the test method's metadata.
+     * If any `@FeatureFlag` annotation is found, it passes every feature flag's name
+     * and enabled state into the user-specified lambda to apply custom actions.
+     */
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                val testMethod = description.testClass.getMethod(description.methodName)
+                val featureFlagAnnotations = testMethod.getAnnotationsByType(
+                    FeatureFlag::class.java
+                )
+
+                val valuesToBeRestored = mutableMapOf<String, Boolean?>()
+                for (featureFlagAnnotation in featureFlagAnnotations) {
+                    valuesToBeRestored[featureFlagAnnotation.name] =
+                            getFlagsMethod(featureFlagAnnotation.name)
+                    setFlagsMethod(featureFlagAnnotation.name, featureFlagAnnotation.enabled)
+                }
+
+                // Execute the test method, which includes methods annotated with
+                // @Before, @Test and @After.
+                base.evaluate()
+
+                valuesToBeRestored.forEach {
+                    setFlagsMethod(it.key, it.value)
+                }
+            }
+        }
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index 05c0444..ae43c15 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -44,13 +44,18 @@
 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 DEFAULT_TAG = RecorderCallback::class.simpleName
+    ?: fail("Could not determine class name")
 
-    private val TAG = this::class.simpleName
+open class RecorderCallback private constructor(
+    private val backingRecord: ArrayTrackRecord<CallbackEntry>,
+    val logTag: String
+) : NetworkCallback() {
+    public constructor(logTag: String = DEFAULT_TAG) : this(ArrayTrackRecord(), logTag)
+    protected constructor(src: RecorderCallback?, logTag: String) : this(
+        src?.backingRecord ?: ArrayTrackRecord(),
+        logTag
+    )
 
     sealed class CallbackEntry {
         // To get equals(), hashcode(), componentN() etc for free, the child classes of
@@ -91,6 +96,7 @@
             override val network: Network,
             val reason: Int
         ) : CallbackEntry()
+
         // Convenience constants for expecting a type
         companion object {
             @JvmField
@@ -122,7 +128,7 @@
     val mark get() = history.mark
 
     override fun onAvailable(network: Network) {
-        Log.d(TAG, "onAvailable $network")
+        Log.d(logTag, "onAvailable $network")
         history.add(Available(network))
     }
 
@@ -130,22 +136,22 @@
     // 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")
+        Log.d(logTag, "onCapabilitiesChanged $network $caps")
         history.add(CapabilitiesChanged(network, caps))
     }
 
     override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) {
-        Log.d(TAG, "onLinkPropertiesChanged $network $lp")
+        Log.d(logTag, "onLinkPropertiesChanged $network $lp")
         history.add(LinkPropertiesChanged(network, lp))
     }
 
     override fun onLocalNetworkInfoChanged(network: Network, info: LocalNetworkInfo) {
-        Log.d(TAG, "onLocalNetworkInfoChanged $network $info")
+        Log.d(logTag, "onLocalNetworkInfoChanged $network $info")
         history.add(LocalInfoChanged(network, info))
     }
 
     override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
-        Log.d(TAG, "onBlockedStatusChanged $network $blocked")
+        Log.d(logTag, "onBlockedStatusChanged $network $blocked")
         history.add(BlockedStatus(network, blocked))
     }
 
@@ -153,27 +159,27 @@
     // 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")
+        Log.d(logTag, "onNetworkSuspended $network $network")
         history.add(Suspended(network))
     }
 
     override fun onNetworkResumed(network: Network) {
-        Log.d(TAG, "$network onNetworkResumed $network")
+        Log.d(logTag, "$network onNetworkResumed $network")
         history.add(Resumed(network))
     }
 
     override fun onLosing(network: Network, maxMsToLive: Int) {
-        Log.d(TAG, "onLosing $network $maxMsToLive")
+        Log.d(logTag, "onLosing $network $maxMsToLive")
         history.add(Losing(network, maxMsToLive))
     }
 
     override fun onLost(network: Network) {
-        Log.d(TAG, "onLost $network")
+        Log.d(logTag, "onLost $network")
         history.add(Lost(network))
     }
 
     override fun onUnavailable() {
-        Log.d(TAG, "onUnavailable")
+        Log.d(logTag, "onUnavailable")
         history.add(Unavailable())
     }
 }
@@ -187,10 +193,11 @@
  */
 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) {
+    val defaultTimeoutMs: Long,
+    val defaultNoCallbackTimeoutMs: Long,
+    val waiterFunc: Runnable,
+    logTag: String
+) : RecorderCallback(src, logTag) {
     /**
      * Construct a testable network callback.
      * @param timeoutMs the default timeout for expecting a callback. Default 30 seconds. This
@@ -212,11 +219,17 @@
     constructor(
         timeoutMs: Long = DEFAULT_TIMEOUT,
         noCallbackTimeoutMs: Long = DEFAULT_NO_CALLBACK_TIMEOUT,
-        waiterFunc: Runnable = NOOP
-    ) : this(null, timeoutMs, noCallbackTimeoutMs, waiterFunc)
+        waiterFunc: Runnable = NOOP, // "() -> Unit" would forbid calling with a void func from Java
+        logTag: String = DEFAULT_TAG
+    ) : this(null, timeoutMs, noCallbackTimeoutMs, waiterFunc, logTag)
 
     fun createLinkedCopy() = TestableNetworkCallback(
-            this, defaultTimeoutMs, defaultNoCallbackTimeoutMs, waiterFunc)
+        this,
+        defaultTimeoutMs,
+        defaultNoCallbackTimeoutMs,
+        waiterFunc,
+        logTag
+    )
 
     // 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
@@ -355,17 +368,29 @@
         timeoutMs: Long = defaultTimeoutMs,
         errorMsg: String? = null,
         test: (T) -> Boolean = { true }
-    ) = (poll(timeoutMs) ?: fail("Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
+    ) = (poll(timeoutMs) ?: failWithErrorReason(errorMsg,
+        "Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
             .also {
-                if (it !is T) fail("Expected callback ${T::class.simpleName}, got $it")
+                if (it !is T) {
+                    failWithErrorReason(
+                        errorMsg,
+                        "Expected callback ${T::class.simpleName}, got $it"
+                    )
+                }
                 if (ANY_NETWORK !== network && it.network != network) {
-                    fail("Expected network $network for callback : $it")
+                    failWithErrorReason(errorMsg, "Expected network $network for callback : $it")
                 }
                 if (!test(it)) {
-                    fail("${errorMsg ?: "Callback doesn't match predicate"} : $it")
+                    failWithErrorReason(errorMsg, "Callback doesn't match predicate : $it")
                 }
             } as T
 
+    // "Nothing" is the return type to declare a function never returns a value.
+    fun failWithErrorReason(errorMsg: String?, errorReason: String): Nothing {
+        val message = if (errorMsg != null) "$errorMsg : $errorReason" else errorReason
+        fail(message)
+    }
+
     inline fun <reified T : CallbackEntry> expect(
         network: HasNetwork,
         timeoutMs: Long = defaultTimeoutMs,
@@ -402,8 +427,11 @@
         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()}")
+        assertNotNull(
+            it,
+            "Callback ${T::class} not received within ${timeoutMs}ms. " +
+                "Got ${history.backtrace()}"
+        )
     } as T
 
     @JvmOverloads
@@ -412,8 +440,11 @@
         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()}")
+        assertNotNull(
+            it,
+            "Callback ${type.java} not received within ${timeoutMs}ms. " +
+                "Got ${history.backtrace()}"
+        )
     } as T
 
     fun <T : CallbackEntry> eventuallyExpect(
@@ -422,8 +453,11 @@
         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()}")
+        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:
@@ -534,8 +568,13 @@
         blockedReason: Int,
         tmt: Long = defaultTimeoutMs
     ) {
-        expectAvailableCallbacks(net, validated = false, suspended = false,
-                blockedReason = blockedReason, tmt = tmt)
+        expectAvailableCallbacks(
+            net,
+            validated = false,
+            suspended = false,
+            blockedReason = blockedReason,
+            tmt = tmt
+        )
         expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
     }
 
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
similarity index 62%
rename from staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
rename to staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
index 600a623..435fdd8 100644
--- a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -28,9 +28,11 @@
 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"
+private const val IGNORE_WIFI_CHECK = "ignore-wifi-check"
+private const val IGNORE_MOBILE_DATA_CHECK = "ignore-mobile-data-check"
 
 // The default updater package names, which might be updating packages while the CTS
 // are running
@@ -41,14 +43,23 @@
  *
  * 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".
+ * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-mobile-data-check:true". \
+ * --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-wifi-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
+    @Option(
+        name = IGNORE_WIFI_CHECK,
+            description = "Disables the check for wifi"
+    )
+    private var ignoreWifiCheck = false
+    @Option(
+        name = IGNORE_MOBILE_DATA_CHECK,
+            description = "Disables the check for mobile data"
+    )
+    private var ignoreMobileDataCheck = false
+
     // The default value is never used, but false is a reasonable default
     private var originalTestChainEnabled = false
     private val originalUpdaterPkgsStatus = HashMap<String, Boolean>()
@@ -58,43 +69,62 @@
         disableGmsUpdate(testInfo)
         originalTestChainEnabled = getTestChainEnabled(testInfo)
         originalUpdaterPkgsStatus.putAll(getUpdaterPkgsStatus(testInfo))
-        setUpdaterNetworkingEnabled(testInfo, enableChain = true,
-                enablePkgs = UPDATER_PKGS.associateWith { false })
-        runPreparerApk(testInfo)
+        setUpdaterNetworkingEnabled(
+            testInfo,
+            enableChain = true,
+            enablePkgs = UPDATER_PKGS.associateWith { false }
+        )
+        runConnectivityCheckApk(testInfo)
+        refreshTime(testInfo)
     }
 
-    private fun runPreparerApk(testInfo: TestInformation) {
+    private fun runConnectivityCheckApk(testInfo: TestInformation) {
         installer.setCleanApk(true)
         installer.addTestFileName(CONNECTIVITY_CHECKER_APK)
         installer.setShouldGrantPermission(true)
         installer.setUp(testInfo)
 
+        val testMethods = mutableListOf<String>()
+        if (!ignoreWifiCheck) {
+            testMethods.add("testCheckWifiSetup")
+        }
+        if (!ignoreMobileDataCheck) {
+            testMethods.add("testCheckTelephonySetup")
+        }
+
+        testMethods.forEach {
+            runTestMethod(testInfo, it)
+        }
+    }
+
+    private fun runTestMethod(testInfo: TestInformation, method: String) {
         val runner = DefaultRemoteAndroidTestRunner(
-                CONNECTIVITY_PKG_NAME,
-                CONNECTIVITY_CHECK_RUNNER_NAME,
-                testInfo.device.iDevice)
+            CONNECTIVITY_PKG_NAME,
+            CONNECTIVITY_CHECK_RUNNER_NAME,
+            testInfo.device.iDevice
+        )
         runner.runOptions = "--no-hidden-api-checks"
+        runner.setMethodName(CONNECTIVITY_CHECK_CLASS, method)
 
         val receiver = CollectingTestListener()
         if (!testInfo.device.runInstrumentationTests(runner, receiver)) {
-            throw TargetSetupError("Device state check failed to complete",
-                    testInfo.device.deviceDescriptor)
+            throw TargetSetupError(
+                "Device state check failed to complete",
+                testInfo.device.deviceDescriptor
+            )
         }
 
         val runResult = receiver.currentRunResults
         if (runResult.isRunFailure) {
-            throw TargetSetupError("Failed to check device state before the test: " +
-                    runResult.runFailureMessage, testInfo.device.deviceDescriptor)
-        }
-
-        val ignoredTestClasses = mutableSetOf<String>()
-        if (ignoreConnectivityCheck) {
-            ignoredTestClasses.add(CONNECTIVITY_CHECK_CLASS)
+            throw TargetSetupError(
+                "Failed to check device state before the test: " +
+                    runResult.runFailureMessage,
+                testInfo.device.deviceDescriptor
+            )
         }
 
         val errorMsg = runResult.testResults.mapNotNull { (testDescription, testResult) ->
-            if (TestResult.TestStatus.FAILURE != testResult.status ||
-                    ignoredTestClasses.contains(testDescription.className)) {
+            if (TestResult.TestStatus.FAILURE != testResult.status) {
                 null
             } else {
                 "$testDescription: ${testResult.stackTrace}"
@@ -102,21 +132,27 @@
         }.joinToString("\n")
         if (errorMsg.isBlank()) return
 
-        throw TargetSetupError("Device setup checks failed. Check the test bench: \n$errorMsg",
-                testInfo.device.deviceDescriptor)
+        throw TargetSetupError(
+            "Device setup checks failed. Check the test bench: \n$errorMsg",
+            testInfo.device.deviceDescriptor
+        )
     }
 
     private fun disableGmsUpdate(testInfo: TestInformation) {
         // This will be a no-op on devices without root (su) or not using gservices, but that's OK.
-        testInfo.exec("su 0 am broadcast " +
+        testInfo.exec(
+            "su 0 am broadcast " +
                 "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
-                "-e finsky.play_services_auto_update_enabled false")
+                "-e finsky.play_services_auto_update_enabled false"
+        )
     }
 
     private fun clearGmsUpdateOverride(testInfo: TestInformation) {
-        testInfo.exec("su 0 am broadcast " +
+        testInfo.exec(
+            "su 0 am broadcast " +
                 "-a com.google.gservices.intent.action.GSERVICES_OVERRIDE " +
-                "--esn finsky.play_services_auto_update_enabled")
+                "--esn finsky.play_services_auto_update_enabled"
+        )
     }
 
     private fun setUpdaterNetworkingEnabled(
@@ -136,17 +172,27 @@
             testInfo.exec("cmd connectivity get-chain3-enabled").contains("chain:enabled")
 
     private fun getUpdaterPkgsStatus(testInfo: TestInformation) =
-            UPDATER_PKGS.associateWith { pkg ->
-                !testInfo.exec("cmd connectivity get-package-networking-enabled $pkg")
-                        .contains(":deny")
-            }
+        UPDATER_PKGS.associateWith { pkg ->
+            !testInfo.exec("cmd connectivity get-package-networking-enabled $pkg")
+                .contains(":deny")
+        }
+
+    private fun refreshTime(testInfo: TestInformation,) {
+        // Forces a synchronous time refresh using the network. Time is fetched synchronously but
+        // this does not guarantee that system time is updated when it returns.
+        // This avoids flakes where the system clock rolls back, for example when using test
+        // settings like test_url_expiration_time in NetworkMonitor.
+        testInfo.exec("cmd network_time_update_service force_refresh")
+    }
 
     override fun tearDown(testInfo: TestInformation, e: Throwable?) {
         if (isTearDownDisabled) return
         installer.tearDown(testInfo, e)
-        setUpdaterNetworkingEnabled(testInfo,
-                enableChain = originalTestChainEnabled,
-                enablePkgs = originalUpdaterPkgsStatus)
+        setUpdaterNetworkingEnabled(
+            testInfo,
+            enableChain = originalTestChainEnabled,
+            enablePkgs = originalUpdaterPkgsStatus
+        )
         clearGmsUpdateOverride(testInfo)
     }
 }
diff --git a/staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/DisableConfigSyncTargetPreparer.kt
similarity index 100%
rename from staticlibs/testutils/host/com/android/testutils/DisableConfigSyncTargetPreparer.kt
rename to staticlibs/testutils/host/java/com/android/testutils/DisableConfigSyncTargetPreparer.kt
diff --git a/staticlibs/testutils/host/python/adb_utils.py b/staticlibs/testutils/host/python/adb_utils.py
new file mode 100644
index 0000000..13c0646
--- /dev/null
+++ b/staticlibs/testutils/host/python/adb_utils.py
@@ -0,0 +1,118 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import re
+from mobly.controllers import android_device
+from net_tests_utils.host.python import assert_utils
+
+BYTE_DECODE_UTF_8 = "utf-8"
+
+
+def set_doze_mode(ad: android_device.AndroidDevice, enable: bool) -> None:
+  if enable:
+    adb_shell(ad, "cmd battery unplug")
+    expect_dumpsys_state_with_retry(
+        ad, "deviceidle", key="mCharging", expected_state=False
+    )
+    _set_screen_state(ad, False)
+    adb_shell(ad, "dumpsys deviceidle enable deep")
+    expect_dumpsys_state_with_retry(
+        ad, "deviceidle", key="mDeepEnabled", expected_state=True
+    )
+    adb_shell(ad, "dumpsys deviceidle force-idle deep")
+    expect_dumpsys_state_with_retry(
+        ad, "deviceidle", key="mForceIdle", expected_state=True
+    )
+  else:
+    adb_shell(ad, "cmd battery reset")
+    expect_dumpsys_state_with_retry(
+        ad, "deviceidle", key="mCharging", expected_state=True
+    )
+    adb_shell(ad, "dumpsys deviceidle unforce")
+    expect_dumpsys_state_with_retry(
+        ad, "deviceidle", key="mForceIdle", expected_state=False
+    )
+
+
+def _set_screen_state(
+    ad: android_device.AndroidDevice, target_state: bool
+) -> None:
+  assert_utils.expect_with_retry(
+      predicate=lambda: _get_screen_state(ad) == target_state,
+      retry_action=lambda: adb_shell(
+          ad, "input keyevent KEYCODE_POWER"
+      ),  # Toggle power key again when retry.
+  )
+
+
+def _get_screen_state(ad: android_device.AndroidDevice) -> bool:
+  return get_value_of_key_from_dumpsys(ad, "power", "mWakefulness") == "Awake"
+
+
+def get_value_of_key_from_dumpsys(
+    ad: android_device.AndroidDevice, service: str, key: str
+) -> str:
+  output = get_dumpsys_for_service(ad, service)
+  # Search for key=value pattern from the dumpsys output.
+  # e.g. mWakefulness=Awake
+  pattern = rf"{key}=(.*)"
+  # Only look for the first occurrence.
+  match = re.search(pattern, output)
+  if match:
+    ad.log.debug(
+        "Getting key-value from dumpsys: " + key + "=" + match.group(1)
+    )
+    return match.group(1)
+  else:
+    return None
+
+
+def expect_dumpsys_state_with_retry(
+    ad: android_device.AndroidDevice,
+    service: str,
+    key: str,
+    expected_state: bool,
+    retry_interval_sec: int = 1,
+) -> None:
+  def predicate():
+    value = get_value_of_key_from_dumpsys(ad, service, key)
+    if value is None:
+      return False
+    return value.lower() == str(expected_state).lower()
+
+  assert_utils.expect_with_retry(
+      predicate=predicate,
+      retry_interval_sec=retry_interval_sec,
+  )
+
+
+def get_dumpsys_for_service(
+    ad: android_device.AndroidDevice, service: str
+) -> str:
+  return adb_shell(ad, "dumpsys " + service)
+
+
+def adb_shell(ad: android_device.AndroidDevice, shell_cmd: str) -> str:
+  """Runs adb shell command.
+
+  Args:
+    ad: Android device object.
+    shell_cmd: string of list of strings, adb shell command.
+
+  Returns:
+    string, replies from adb shell command.
+  """
+  ad.log.debug("Executing adb shell %s", shell_cmd)
+  data = ad.adb.shell(shell_cmd)
+  return data.decode(BYTE_DECODE_UTF_8).strip()
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
new file mode 100644
index 0000000..7203265
--- /dev/null
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -0,0 +1,80 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import asserts
+from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python.tether_utils import UpstreamType
+
+
+class ApfTestBase(multi_devices_test_base.MultiDevicesTestBase):
+
+  def setup_class(self):
+    super().setup_class()
+
+    # Check test preconditions.
+    tether_utils.assume_hotspot_test_preconditions(
+        self.serverDevice, self.clientDevice, UpstreamType.NONE
+    )
+    asserts.abort_class_if(
+        not apf_utils.is_send_raw_packet_downstream_supported(
+            self.serverDevice
+        ),
+        "NetworkStack is too old to support send raw packet, skip test.",
+    )
+
+    # Fetch device properties and storing them locally for later use.
+    client = self.clientDevice.connectivity_multi_devices_snippet
+    self.server_iface_name, client_network = (
+        tether_utils.setup_hotspot_and_client_for_upstream_type(
+            self.serverDevice, self.clientDevice, UpstreamType.NONE
+        )
+    )
+    self.client_iface_name = client.getInterfaceNameFromNetworkHandle(
+        client_network
+    )
+    self.server_mac_address = apf_utils.get_hardware_address(
+        self.serverDevice, self.server_iface_name
+    )
+
+    # Enable doze mode to activate APF.
+    adb_utils.set_doze_mode(self.clientDevice, True)
+
+  def teardown_class(self):
+    adb_utils.set_doze_mode(self.clientDevice, False)
+    tether_utils.cleanup_tethering_for_upstream_type(
+        self.serverDevice, UpstreamType.NONE
+    )
+
+  def send_packet_and_expect_counter_increased(
+      self, packet: str, counter_name: str
+  ) -> None:
+    count_before_test = apf_utils.get_apf_counter(
+        self.clientDevice,
+        self.client_iface_name,
+        counter_name,
+    )
+    apf_utils.send_raw_packet_downstream(
+        self.serverDevice, self.server_iface_name, packet
+    )
+
+    assert_utils.expect_with_retry(
+        lambda: apf_utils.get_apf_counter(
+            self.clientDevice,
+            self.client_iface_name,
+            counter_name,
+        )
+        > count_before_test
+    )
+
+    # TODO: Verify the packet is not actually received.
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
new file mode 100644
index 0000000..a3ec6e9
--- /dev/null
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -0,0 +1,243 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from dataclasses import dataclass
+import re
+from mobly import asserts
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib.adb import AdbError
+from net_tests_utils.host.python import adb_utils, assert_utils
+
+
+class PatternNotFoundException(Exception):
+  """Raised when the given pattern cannot be found."""
+
+
+class UnsupportedOperationException(Exception):
+  pass
+
+
+def get_apf_counter(
+    ad: android_device.AndroidDevice, iface: str, counter_name: str
+) -> int:
+  counters = get_apf_counters_from_dumpsys(ad, iface)
+  return counters.get(counter_name, 0)
+
+
+def get_apf_counters_from_dumpsys(
+    ad: android_device.AndroidDevice, iface_name: str
+) -> dict:
+  dumpsys = adb_utils.get_dumpsys_for_service(ad, "network_stack")
+
+  # Extract IpClient section of the specified interface.
+  # This takes inputs like:
+  # IpClient.wlan0
+  #   ...
+  # IpClient.wlan1
+  #   ...
+  iface_pattern = re.compile(
+      r"^IpClient\." + iface_name + r"\n" + r"((^\s.*\n)+)", re.MULTILINE
+  )
+  iface_result = iface_pattern.search(dumpsys)
+  if iface_result is None:
+    raise PatternNotFoundException("Cannot find IpClient for " + iface_name)
+
+  # Extract APF counters section from IpClient section, which looks like:
+  #     APF packet counters:
+  #       COUNTER_NAME: VALUE
+  #       ....
+  apf_pattern = re.compile(
+      r"APF packet counters:.*\n.(\s+[A-Z_0-9]+: \d+\n)+", re.MULTILINE
+  )
+  apf_result = apf_pattern.search(iface_result.group(0))
+  if apf_result is None:
+    raise PatternNotFoundException(
+        "Cannot find APF counters in text: " + iface_result.group(0)
+    )
+
+  # Extract key-value pairs from APF counters section into a list of tuples,
+  # e.g. [('COUNTER1', '1'), ('COUNTER2', '2')].
+  counter_pattern = re.compile(r"(?P<name>[A-Z_0-9]+): (?P<value>\d+)")
+  counter_result = counter_pattern.findall(apf_result.group(0))
+  if counter_result is None:
+    raise PatternNotFoundException(
+        "Cannot extract APF counters in text: " + apf_result.group(0)
+    )
+
+  # Convert into a dict.
+  result = {}
+  for key, value_str in counter_result:
+    result[key] = int(value_str)
+
+  ad.log.debug("Getting apf counters: " + str(result))
+  return result
+
+
+def get_hardware_address(
+    ad: android_device.AndroidDevice, iface_name: str
+) -> str:
+  """Retrieves the hardware (MAC) address for a given network interface.
+
+  Returns:
+      The hex representative of the MAC address in uppercase.
+      E.g. 12:34:56:78:90:AB
+
+  Raises:
+      PatternNotFoundException: If the MAC address is not found in the command
+      output.
+  """
+
+  # Run the "ip link" command and get its output.
+  ip_link_output = adb_utils.adb_shell(ad, f"ip link show {iface_name}")
+
+  # Regular expression to extract the MAC address.
+  # Parse hardware address from ip link output like below:
+  # 46: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq ...
+  #    link/ether 72:05:77:82:21:e0 brd ff:ff:ff:ff:ff:ff
+  pattern = r"link/ether (([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})"
+  match = re.search(pattern, ip_link_output)
+
+  if match:
+    return match.group(1).upper()  # Extract the MAC address string.
+  else:
+    raise PatternNotFoundException(
+        "Cannot get hardware address for " + iface_name
+    )
+
+
+def is_send_raw_packet_downstream_supported(
+    ad: android_device.AndroidDevice,
+) -> bool:
+  try:
+    # Invoke the shell command with empty argument and see how NetworkStack respond.
+    # If supported, an IllegalArgumentException with help page will be printed.
+    send_raw_packet_downstream(ad, "", "")
+  except assert_utils.UnexpectedBehaviorError:
+    return True
+  except UnsupportedOperationException:
+    return False
+
+
+def send_raw_packet_downstream(
+    ad: android_device.AndroidDevice,
+    iface_name: str,
+    packet_in_hex: str,
+) -> None:
+  """Sends a raw packet over the specified downstream interface.
+
+  This function constructs and sends a raw packet using the
+  `send-raw-packet-downstream`
+  command provided by NetworkStack process. It's primarily intended for testing
+  purposes.
+
+  Args:
+      ad: The AndroidDevice object representing the connected device.
+      iface_name: The name of the network interface to use (e.g., "wlan0",
+        "eth0").
+      packet_in_hex: The raw packet data starting from L2 header encoded in
+        hexadecimal string format.
+
+  Raises:
+      UnsupportedOperationException: If the NetworkStack doesn't support
+        the `send-raw-packet` command.
+      UnexpectedBehaviorException: If the command execution produces unexpected
+        output other than an empty response or "Unknown command".
+
+  Important Considerations:
+      Security: This method only works on tethering downstream interfaces due
+        to security restrictions.
+      Packet Format: The `packet_in_hex` must be a valid hexadecimal
+        representation of a packet starting from L2 header.
+  """
+
+  cmd = (
+      "cmd network_stack send-raw-packet-downstream"
+      f" {iface_name} {packet_in_hex}"
+  )
+
+  # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+  try:
+    output = adb_utils.adb_shell(ad, cmd)
+  except AdbError as e:
+    output = str(e.stdout)
+  if output:
+    if "Unknown command" in output:
+      raise UnsupportedOperationException(
+          "send-raw-packet-downstream command is not supported."
+      )
+    raise assert_utils.UnexpectedBehaviorError(
+        f"Got unexpected output: {output} for command: {cmd}."
+    )
+
+
+@dataclass
+class ApfCapabilities:
+  """APF program support capabilities.
+
+  See android.net.apf.ApfCapabilities.
+
+  Attributes:
+      apf_version_supported (int): Version of APF instruction set supported for
+        packet filtering. 0 indicates no support for packet filtering using APF
+        programs.
+      apf_ram_size (int): Size of APF ram.
+      apf_packet_format (int): Format of packets passed to APF filter. Should be
+        one of ARPHRD_*
+  """
+
+  apf_version_supported: int
+  apf_ram_size: int
+  apf_packet_format: int
+
+  def __init__(
+      self,
+      apf_version_supported: int,
+      apf_ram_size: int,
+      apf_packet_format: int,
+  ):
+    self.apf_version_supported = apf_version_supported
+    self.apf_ram_size = apf_ram_size
+    self.apf_packet_format = apf_packet_format
+
+  def __str__(self):
+    """Returns a user-friendly string representation of the APF capabilities."""
+    return (
+        f"APF Version: {self.apf_version_supported}\n"
+        f"Ram Size: {self.apf_ram_size} bytes\n"
+        f"Packet Format: {self.apf_packet_format}"
+    )
+
+
+def get_apf_capabilities(
+    ad: android_device.AndroidDevice, iface_name: str
+) -> ApfCapabilities:
+  output = adb_utils.adb_shell(
+      ad, f"cmd network_stack apf {iface_name} capabilities"
+  )
+  try:
+    values = [int(value_str) for value_str in output.split(",")]
+  except ValueError:
+    return ApfCapabilities(0, 0, 0)  # Conversion to integer failed
+  return ApfCapabilities(values[0], values[1], values[2])
+
+
+def assume_apf_version_support_at_least(
+    ad: android_device.AndroidDevice, iface_name: str, expected_version: int
+) -> None:
+  caps = get_apf_capabilities(ad, iface_name)
+  asserts.skip_if(
+      caps.apf_version_supported < expected_version,
+      f"Supported apf version {caps.apf_version_supported} < expected version"
+      f" {expected_version}",
+  )
diff --git a/staticlibs/testutils/host/python/assert_utils.py b/staticlibs/testutils/host/python/assert_utils.py
new file mode 100644
index 0000000..da1bb9e
--- /dev/null
+++ b/staticlibs/testutils/host/python/assert_utils.py
@@ -0,0 +1,43 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import time
+from typing import Callable
+
+
+class UnexpectedBehaviorError(Exception):
+  """Raised when there is an unexpected behavior during applying a procedure."""
+
+
+def expect_with_retry(
+    predicate: Callable[[], bool],
+    retry_action: Callable[[], None] = None,
+    max_retries: int = 10,
+    retry_interval_sec: int = 1,
+) -> None:
+  """Executes a predicate and retries if it doesn't return True."""
+
+  for retry in range(max_retries):
+    if predicate():
+      return None
+    else:
+      if retry == max_retries - 1:
+        break
+      if retry_action:
+        retry_action()
+      time.sleep(retry_interval_sec)
+
+  raise UnexpectedBehaviorError(
+      "Predicate didn't become true after " + str(max_retries) + " retries."
+  )
diff --git a/staticlibs/testutils/host/python/mdns_utils.py b/staticlibs/testutils/host/python/mdns_utils.py
new file mode 100644
index 0000000..1234e54
--- /dev/null
+++ b/staticlibs/testutils/host/python/mdns_utils.py
@@ -0,0 +1,57 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import asserts
+from mobly.controllers import android_device
+
+
+def assume_mdns_test_preconditions(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  advertising = advertising_device.connectivity_multi_devices_snippet
+  discovery = discovery_device.connectivity_multi_devices_snippet
+
+  asserts.skip_if(
+      not advertising.isAtLeastT(), "Advertising device SDK is lower than T."
+  )
+  asserts.skip_if(
+      not discovery.isAtLeastT(), "Discovery device SDK is lower than T."
+  )
+
+
+def register_mdns_service_and_discover_resolve(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  """Test mdns advertising, discovery and resolution
+
+  One device registers an mDNS service, and another device discovers and
+  resolves that service.
+  """
+  advertising = advertising_device.connectivity_multi_devices_snippet
+  discovery = discovery_device.connectivity_multi_devices_snippet
+
+  # Register a mDns service
+  advertising.registerMDnsService()
+
+  # Ensure the discovery and resolution of the mDNS service
+  discovery.ensureMDnsServiceDiscoveryAndResolution()
+
+
+def cleanup_mdns_service(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  # Unregister the mDns service
+  advertising_device.connectivity_multi_devices_snippet.unregisterMDnsService()
+  # Stop discovery
+  discovery_device.connectivity_multi_devices_snippet.stopMDnsServiceDiscovery()
diff --git a/staticlibs/testutils/host/python/multi_devices_test_base.py b/staticlibs/testutils/host/python/multi_devices_test_base.py
new file mode 100644
index 0000000..f8a92f3
--- /dev/null
+++ b/staticlibs/testutils/host/python/multi_devices_test_base.py
@@ -0,0 +1,54 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import base_test
+from mobly import utils
+from mobly.controllers import android_device
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+class MultiDevicesTestBase(base_test.BaseTestClass):
+
+  def setup_class(self):
+    # Declare that two Android devices are needed.
+    self.clientDevice, self.serverDevice = self.register_controller(
+        android_device, min_number=2
+    )
+
+    def setup_device(device):
+      device.load_snippet(
+          "connectivity_multi_devices_snippet",
+          CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
+      )
+
+    # Set up devices in parallel to save time.
+    utils.concurrent_exec(
+        setup_device,
+        ((self.clientDevice,), (self.serverDevice,)),
+        max_workers=2,
+        raise_on_exception=True,
+    )
diff --git a/staticlibs/testutils/host/python/tether_utils.py b/staticlibs/testutils/host/python/tether_utils.py
new file mode 100644
index 0000000..702b596
--- /dev/null
+++ b/staticlibs/testutils/host/python/tether_utils.py
@@ -0,0 +1,110 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import base64
+import uuid
+
+from mobly import asserts
+from mobly.controllers import android_device
+
+
+class UpstreamType:
+  NONE = 0
+  CELLULAR = 1
+  WIFI = 2
+
+
+def generate_uuid32_base64() -> str:
+  """Generates a UUID32 and encodes it in Base64.
+
+  Returns:
+      str: The Base64-encoded UUID32 string. Which is 22 characters.
+  """
+  # Strip padding characters to make it safer for hotspot name length limit.
+  return base64.b64encode(uuid.uuid1().bytes).decode("utf-8").strip("=")
+
+
+def assume_hotspot_test_preconditions(
+    server_device: android_device,
+    client_device: android_device,
+    upstream_type: UpstreamType,
+) -> None:
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
+  # Assert pre-conditions specific to each upstream type.
+  asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+  asserts.skip_if(
+      not server.hasHotspotFeature(), "Server requires hotspot feature"
+  )
+  if upstream_type == UpstreamType.CELLULAR:
+    asserts.skip_if(
+        not server.hasTelephonyFeature(), "Server requires Telephony feature"
+    )
+  elif upstream_type == UpstreamType.WIFI:
+    asserts.skip_if(
+        not server.isStaApConcurrencySupported(),
+        "Server requires Wifi AP + STA concurrency",
+    )
+  elif upstream_type == UpstreamType.NONE:
+    pass
+  else:
+    raise ValueError(f"Invalid upstream type: {upstream_type}")
+
+
+def setup_hotspot_and_client_for_upstream_type(
+    server_device: android_device,
+    client_device: android_device,
+    upstream_type: UpstreamType,
+) -> (str, int):
+  """Setup the hotspot with a connected client with the specified upstream type.
+
+  This creates a hotspot, make the client connect
+  to it, and verify the packet is forwarded by the hotspot.
+  And returns interface name of both if successful.
+  """
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
+  if upstream_type == UpstreamType.CELLULAR:
+    server.requestCellularAndEnsureDefault()
+  elif upstream_type == UpstreamType.WIFI:
+    server.ensureWifiIsDefault()
+  elif upstream_type == UpstreamType.NONE:
+    pass
+  else:
+    raise ValueError(f"Invalid upstream type: {upstream_type}")
+
+  # Generate ssid/passphrase with random characters to make sure nearby devices won't
+  # connect unexpectedly. Note that total length of ssid cannot go over 32.
+  test_ssid = "HOTSPOT-" + generate_uuid32_base64()
+  test_passphrase = generate_uuid32_base64()
+
+  # Create a hotspot with fixed SSID and password.
+  hotspot_interface = server.startHotspot(test_ssid, test_passphrase)
+
+  # Make the client connects to the hotspot.
+  client_network = client.connectToWifi(test_ssid, test_passphrase)
+
+  return hotspot_interface, client_network
+
+
+def cleanup_tethering_for_upstream_type(
+    server_device: android_device, upstream_type: UpstreamType
+) -> None:
+  server = server_device.connectivity_multi_devices_snippet
+  if upstream_type == UpstreamType.CELLULAR:
+    server.unregisterAll()
+  # Teardown the hotspot.
+  server.stopAllTethering()
diff --git a/staticlibs/testutils/host/python/wifip2p_utils.py b/staticlibs/testutils/host/python/wifip2p_utils.py
new file mode 100644
index 0000000..8b4ffa5
--- /dev/null
+++ b/staticlibs/testutils/host/python/wifip2p_utils.py
@@ -0,0 +1,50 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly import asserts
+from mobly.controllers import android_device
+
+
+def assume_wifi_p2p_test_preconditions(
+    server_device: android_device, client_device: android_device
+) -> None:
+  server = server_device.connectivity_multi_devices_snippet
+  client = client_device.connectivity_multi_devices_snippet
+
+  # Assert pre-conditions
+  asserts.skip_if(not server.hasWifiFeature(), "Server requires Wifi feature")
+  asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+  asserts.skip_if(
+      not server.isP2pSupported(), "Server requires Wi-fi P2P feature"
+  )
+  asserts.skip_if(
+      not client.isP2pSupported(), "Client requires Wi-fi P2P feature"
+  )
+
+
+def setup_wifi_p2p_server_and_client(
+    server_device: android_device, client_device: android_device
+) -> None:
+  """Set up the Wi-Fi P2P server and client."""
+  # Start Wi-Fi P2P on both server and client.
+  server_device.connectivity_multi_devices_snippet.startWifiP2p()
+  client_device.connectivity_multi_devices_snippet.startWifiP2p()
+
+
+def cleanup_wifi_p2p(
+    server_device: android_device, client_device: android_device
+) -> None:
+  # Stop Wi-Fi P2P
+  server_device.connectivity_multi_devices_snippet.stopWifiP2p()
+  client_device.connectivity_multi_devices_snippet.stopWifiP2p()
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
index af4f96d..c6e5f25 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
@@ -19,10 +19,77 @@
 package com.android.testutils
 
 import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
 import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
 import kotlin.system.measureTimeMillis
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
 
 // For Java usage
 fun durationOf(fn: Runnable) = measureTimeMillis { fn.run() }
 
 fun CountDownLatch.await(timeoutMs: Long): Boolean = await(timeoutMs, TimeUnit.MILLISECONDS)
+
+/**
+ * Quit resources provided as a list by a supplier.
+ *
+ * The supplier may return more resources as the process progresses, for example while interrupting
+ * threads and waiting for them to finish they may spawn more threads, so this implements a
+ * [maxRetryCount] which, in this case, would be the maximum length of the thread chain that can be
+ * terminated.
+ */
+fun <T> quitResources(
+    maxRetryCount: Int,
+    supplier: () -> List<T>,
+    terminator: Consumer<T>
+) {
+    // Run it multiple times since new threads might be generated in a thread
+    // that is about to be terminated
+    for (retryCount in 0 until maxRetryCount) {
+        val resourcesToBeCleared = supplier()
+        if (resourcesToBeCleared.isEmpty()) return
+        for (resource in resourcesToBeCleared) {
+            terminator.accept(resource)
+        }
+    }
+    assertEmpty(supplier())
+}
+
+/**
+ * Implementation of [quitResources] to interrupt and wait for [ExecutorService]s to finish.
+ */
+@JvmOverloads
+fun quitExecutorServices(
+    maxRetryCount: Int,
+    interrupt: Boolean = true,
+    timeoutMs: Long = 10_000L,
+    supplier: () -> List<ExecutorService>
+) {
+    quitResources(maxRetryCount, supplier) { ecs ->
+        if (interrupt) {
+            ecs.shutdownNow()
+        }
+        assertTrue(ecs.awaitTermination(timeoutMs, TimeUnit.MILLISECONDS),
+            "ExecutorServices did not terminate within timeout")
+    }
+}
+
+/**
+ * Implementation of [quitResources] to interrupt and wait for [Thread]s to finish.
+ */
+@JvmOverloads
+fun quitThreads(
+    maxRetryCount: Int,
+    interrupt: Boolean = true,
+    timeoutMs: Long = 10_000L,
+    supplier: () -> List<Thread>
+) {
+    quitResources(maxRetryCount, supplier) { th ->
+        if (interrupt) {
+            th.interrupt()
+        }
+        th.join(timeoutMs)
+        assertFalse(th.isAlive, "Threads did not terminate within timeout.")
+    }
+}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 6e9d614..e95a81a 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -201,3 +201,8 @@
     name: "connectivity-mainline-presubmit-java-defaults",
     test_mainline_modules: mainline_presubmit_modules,
 }
+
+filegroup {
+    name: "connectivity_mainline_test_map",
+    srcs: ["connectivity_mainline_test.map"],
+}
diff --git a/tests/common/connectivity_mainline_test.map b/tests/common/connectivity_mainline_test.map
new file mode 100644
index 0000000..043312e
--- /dev/null
+++ b/tests/common/connectivity_mainline_test.map
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Some connectivity tests run on older OS versions, and for those tests, many
+# library dependencies (such as libbase and libc++) need to be linked
+# statically. The tests also need to be linked with a version script to ensure
+# that the statically-linked library isn't exported from the executable, where
+# it would override the shared libraries that the OS itself uses. See
+# b/333438055 for an example of what goes wrong when libc++ is partially
+# exported from an executable.
+{
+  local:
+    *;
+};
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 6eb56c7b..0f0e2f1 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -61,6 +61,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.os.Process.INVALID_UID;
+
 import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
@@ -68,6 +69,7 @@
 import static com.android.testutils.MiscAsserts.assertEmpty;
 import static com.android.testutils.MiscAsserts.assertThrows;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -83,21 +85,25 @@
 import android.os.Build;
 import android.util.ArraySet;
 import android.util.Range;
+
 import androidx.test.filters.SmallTest;
+
 import com.android.testutils.CompatUtil;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
 // NetworkCapabilities is only updatable on S+, and this test covers behavior which implementation
@@ -1465,4 +1471,26 @@
         assertEquals("-SUPL-VALIDATED-CAPTIVE_PORTAL+MMS+OEM_PAID",
                 nc1.describeCapsDifferencesFrom(nc2));
     }
+
+    @Test
+    public void testInvalidCapability() {
+        final int invalidCapability = Integer.MAX_VALUE;
+        // Passing invalid capability does not throw
+        final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING)
+                .removeCapability(invalidCapability)
+                .removeForbiddenCapability(invalidCapability)
+                .addCapability(invalidCapability)
+                .addForbiddenCapability(invalidCapability)
+                .build();
+
+        final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING)
+                .build();
+
+        // nc1 and nc2 are the same since invalid capability is ignored
+        assertEquals(nc1, nc2);
+    }
 }
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
index 8cef6aa..17f5e96 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -28,18 +28,26 @@
 import android.net.NetworkStats.SET_DEFAULT
 import android.net.NetworkStats.SET_FOREGROUND
 import android.net.NetworkStats.TAG_NONE
+import android.os.Build
 import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.assertNetworkStatsEquals
 import com.android.testutils.assertParcelingIsLossless
 import kotlin.test.assertEquals
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
-@RunWith(JUnit4::class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 class NetworkStatsApiTest {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
     private val testStatsEmpty = NetworkStats(0L, 0)
 
     // Note that these variables need to be initialized outside of constructor, initialize
@@ -49,6 +57,7 @@
     // be merged if performing add on these 2 stats.
     private lateinit var testStats1: NetworkStats
     private lateinit var testStats2: NetworkStats
+    private lateinit var expectedEntriesInStats2: List<Entry>
 
     // This is a result of adding stats1 and stats2, while the merging of common key items is
     // subject to test later, this should not be initialized with for a loop to add stats1
@@ -84,19 +93,23 @@
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 1, 6, 2, 0))
         assertEquals(8, testStats1.size())
 
-        testStats2 = NetworkStats(0L, 0)
-                // Entries which are common for set1 and set2.
-                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1))
-                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45))
-                .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7))
-                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0))
-                // Entry which only appears in set2.
-                .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+        expectedEntriesInStats2 = listOf(
+            // Entries which are common for set1 and set2.
+            Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
+            Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
+            Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
+            Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
+            // Entry which only appears in set2.
+            Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                  METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+        testStats2 = NetworkStats(0L, 5)
+        for (entry in expectedEntriesInStats2) {
+            testStats2 = testStats2.addEntry(entry)
+        }
         assertEquals(5, testStats2.size())
 
         testStats3 = NetworkStats(0L, 9)
@@ -125,18 +138,6 @@
 
     @Test
     fun testAddEntry() {
-        val expectedEntriesInStats2 = arrayOf(
-                Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
-                Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
-                Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
-                Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
-                Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
-
         // While testStats* are already initialized with addEntry, verify content added
         // matches expectation.
         for (i in expectedEntriesInStats2.indices) {
@@ -150,6 +151,27 @@
         assertEquals(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
                 METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16, -2, 9, 1, 9),
                 stats.getValues(3, null))
+
+        // Verify the original ststs object is not altered.
+        for (i in expectedEntriesInStats2.indices) {
+            val entry = testStats2.getValues(i, null)
+            assertEquals(expectedEntriesInStats2[i], entry)
+        }
+    }
+
+    @ConnectivityModuleTest
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2) // Mainlined NetworkStats only runs on T+
+    @Test
+    fun testAddEntries() {
+        val baseStats = NetworkStats(0L, 1)
+                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+        val statsUnderTest = baseStats.addEntries(expectedEntriesInStats2)
+        // Assume the correctness of addEntry is verified in other tests.
+        val expectedStats = testStats2
+                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+        assertNetworkStatsEquals(expectedStats, statsUnderTest)
     }
 
     @Test
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
index 8e89037..21e34ab 100644
--- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -16,6 +16,7 @@
 
 package android.net.nsd;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
@@ -51,6 +52,23 @@
 
     private static final InetAddress IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
     private static final InetAddress IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::");
+    private static final byte[] PUBLIC_KEY_RDATA = new byte[] {
+            (byte) 0x02, (byte)0x01,  // flag
+            (byte) 0x03, // protocol
+            (byte) 0x0d, // algorithm
+            // 64-byte public key below
+            (byte) 0xC1, (byte) 0x41, (byte) 0xD0, (byte) 0x63, (byte) 0x79, (byte) 0x60,
+            (byte) 0xB9, (byte) 0x8C, (byte) 0xBC, (byte) 0x12, (byte) 0xCF, (byte) 0xCA,
+            (byte) 0x22, (byte) 0x1D, (byte) 0x28, (byte) 0x79, (byte) 0xDA, (byte) 0xC2,
+            (byte) 0x6E, (byte) 0xE5, (byte) 0xB4, (byte) 0x60, (byte) 0xE9, (byte) 0x00,
+            (byte) 0x7C, (byte) 0x99, (byte) 0x2E, (byte) 0x19, (byte) 0x02, (byte) 0xD8,
+            (byte) 0x97, (byte) 0xC3, (byte) 0x91, (byte) 0xB0, (byte) 0x37, (byte) 0x64,
+            (byte) 0xD4, (byte) 0x48, (byte) 0xF7, (byte) 0xD0, (byte) 0xC7, (byte) 0x72,
+            (byte) 0xFD, (byte) 0xB0, (byte) 0x3B, (byte) 0x1D, (byte) 0x9D, (byte) 0x6D,
+            (byte) 0x52, (byte) 0xFF, (byte) 0x88, (byte) 0x86, (byte) 0x76, (byte) 0x9E,
+            (byte) 0x8E, (byte) 0x23, (byte) 0x62, (byte) 0x51, (byte) 0x35, (byte) 0x65,
+            (byte) 0x27, (byte) 0x09, (byte) 0x62, (byte) 0xD3
+    };
 
     @Test
     public void testLimits() throws Exception {
@@ -120,6 +138,7 @@
         fullInfo.setPort(4242);
         fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
         fullInfo.setHostname("home");
+        fullInfo.setPublicKey(PUBLIC_KEY_RDATA);
         fullInfo.setNetwork(new Network(123));
         fullInfo.setInterfaceIndex(456);
         checkParcelable(fullInfo);
@@ -136,6 +155,7 @@
         attributedInfo.setPort(4242);
         attributedInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
         attributedInfo.setHostname("home");
+        attributedInfo.setPublicKey(PUBLIC_KEY_RDATA);
         attributedInfo.setAttribute("color", "pink");
         attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
         attributedInfo.setAttribute("adorable", (String) null);
@@ -172,6 +192,7 @@
         assertEquals(original.getServiceType(), result.getServiceType());
         assertEquals(original.getHost(), result.getHost());
         assertEquals(original.getHostname(), result.getHostname());
+        assertArrayEquals(original.getPublicKey(), result.getPublicKey());
         assertTrue(original.getPort() == result.getPort());
         assertEquals(original.getNetwork(), result.getNetwork());
         assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS
index cb4ca59..acf506d 100644
--- a/tests/cts/OWNERS
+++ b/tests/cts/OWNERS
@@ -9,4 +9,6 @@
 # For incremental changes on EthernetManagerTest to increase coverage for existing behavior and for
 # testing bug fixes.
 per-file net/src/android/net/cts/EthernetManagerTest.kt = prohr@google.com #{LAST_RESORT_SUGGESTION}
+# Temporary ownership to develop APF CTS tests.
+per-file net/src/android/net/cts/ApfIntegrationTest.kt = prohr@google.com #{LAST_RESORT_SUGGESTION}
 
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index f6c0430..14d5d54 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -29,7 +29,6 @@
     // Only compile source java files in this apk.
     srcs: [
         "src/**/*.java",
-        ":ArgumentConstants",
     ],
     libs: [
         "net-tests-utils-host-device-common",
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index 0ffe81e..ea6b078 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -22,7 +22,6 @@
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
 
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
-    <target_preparer class="com.android.cts.net.NetworkPolicyTestsPreparer" />
 
     <!-- Enabling change id ALLOW_TEST_API_ACCESS allows that package to access @TestApi methods -->
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
@@ -30,7 +29,6 @@
         <option name="teardown-command" value="am compat reset ALLOW_TEST_API_ACCESS com.android.cts.net.hostside.app2" />
         <option name="teardown-command" value="cmd power set-mode 0" />
         <option name="teardown-command" value="cmd battery reset" />
-        <option name="teardown-command" value="cmd netpolicy stop-watching" />
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
diff --git a/tests/cts/hostside/OWNERS b/tests/cts/hostside/OWNERS
deleted file mode 100644
index 20bc55e..0000000
--- a/tests/cts/hostside/OWNERS
+++ /dev/null
@@ -1,4 +0,0 @@
-# Bug component: 61373
-# Inherits parent owners
-sudheersai@google.com
-jchalard@google.com
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 18a5897..33761dc 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -21,9 +21,7 @@
     name: "CtsHostsideNetworkTestsAidl",
     sdk_version: "current",
     srcs: [
-        "com/android/cts/net/hostside/IMyService.aidl",
-        "com/android/cts/net/hostside/INetworkCallback.aidl",
-        "com/android/cts/net/hostside/INetworkStateObserver.aidl",
-        "com/android/cts/net/hostside/IRemoteSocketFactory.aidl",
+        "com/android/cts/net/hostside/*.aidl",
+        "com/android/cts/net/hostside/*.java",
     ],
 }
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
deleted file mode 100644
index e7b2815..0000000
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import android.app.job.JobInfo;
-
-import com.android.cts.net.hostside.INetworkCallback;
-
-interface IMyService {
-    void registerBroadcastReceiver();
-    int getCounters(String receiverName, String action);
-    String checkNetworkStatus();
-    String getRestrictBackgroundStatus();
-    void sendNotification(int notificationId, String notificationType);
-    void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
-    void unregisterNetworkCallback();
-    int scheduleJob(in JobInfo jobInfo);
-}
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
deleted file mode 100644
index 2048bab..0000000
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkCallback.aidl
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import android.net.Network;
-import android.net.NetworkCapabilities;
-
-interface INetworkCallback {
-    void onBlockedStatusChanged(in Network network, boolean blocked);
-    void onAvailable(in Network network);
-    void onLost(in Network network);
-    void onCapabilitiesChanged(in Network network, in NetworkCapabilities cap);
-}
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
deleted file mode 100644
index 19198c5..0000000
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-interface INetworkStateObserver {
-    void onNetworkStateChecked(int resultCode, String resultData);
-
-    const int RESULT_SUCCESS_NETWORK_STATE_CHECKED = 0;
-    const int RESULT_ERROR_UNEXPECTED_PROC_STATE = 1;
-    const int RESULT_ERROR_UNEXPECTED_CAPABILITIES = 2;
-    const int RESULT_ERROR_OTHER = 3;
-}
\ No newline at end of file
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index cf4afa9..919e025 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -38,7 +38,6 @@
     ],
     srcs: [
         "src/**/*.java",
-         ":ArgumentConstants",
     ],
     // Tag this module as a cts test artifact
     test_suites: [
diff --git a/tests/cts/hostside/app/AndroidManifest.xml b/tests/cts/hostside/app/AndroidManifest.xml
index ca3397b..e0f4cdc 100644
--- a/tests/cts/hostside/app/AndroidManifest.xml
+++ b/tests/cts/hostside/app/AndroidManifest.xml
@@ -43,14 +43,6 @@
                 <action android:name="android.net.VpnService"/>
             </intent-filter>
         </service>
-        <service android:name=".MyNotificationListenerService"
-             android:label="MyNotificationListenerService"
-             android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
-             android:exported="true">
-            <intent-filter>
-                <action android:name="android.service.notification.NotificationListenerService"/>
-            </intent-filter>
-        </service>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
deleted file mode 100644
index d9ff539..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractAppIdleTestCase.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
-
-import static org.junit.Assert.assertEquals;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered tests on idle apps.
- */
-@RequiredProperties({APP_STANDBY_MODE})
-abstract class AbstractAppIdleTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        setAppIdle(false);
-        turnBatteryOn();
-
-        registerBroadcastReceiver();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-
-        resetBatteryState();
-        setAppIdle(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_enabled() throws Exception {
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground app doesn't lose access upon enabling it.
-        setAppIdle(true);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        finishActivity();
-        assertAppIdle(false); // verify - not idle anymore, since activity was launched...
-        assertBackgroundNetworkAccess(true);
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        // Same for foreground service.
-        setAppIdle(true);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        stopForegroundService();
-        assertAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        // Set Idle after foreground service start.
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        setAppIdle(true);
-        addPowerSaveModeWhitelist(TEST_PKG);
-        removePowerSaveModeWhitelist(TEST_PKG);
-        assertForegroundServiceNetworkAccess();
-        stopForegroundService();
-        assertAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertAppIdle(false); // verify - not idle anymore, since whitelisted
-        assertBackgroundNetworkAccess(true);
-
-        setAppIdleNoAssert(true);
-        assertAppIdle(false); // app is still whitelisted
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertAppIdle(true); // verify - idle again, once whitelisted was removed
-        assertBackgroundNetworkAccess(false);
-
-        setAppIdle(true);
-        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertAppIdle(false); // verify - not idle anymore, since whitelisted
-        assertBackgroundNetworkAccess(true);
-
-        setAppIdleNoAssert(true);
-        assertAppIdle(false); // app is still whitelisted
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertAppIdle(true); // verify - idle again, once whitelisted was removed
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-
-        // verify - no whitelist, no access!
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_tempWhitelisted() throws Exception {
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-        assertBackgroundNetworkAccess(true);
-        // Wait until the whitelist duration is expired.
-        SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_disabled() throws Exception {
-        assertBackgroundNetworkAccess(true);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    @Test
-    public void testAppIdleNetworkAccess_whenCharging() throws Exception {
-        // Check that app is paroled when charging
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-        turnBatteryOff();
-        assertBackgroundNetworkAccess(true);
-        turnBatteryOn();
-        assertBackgroundNetworkAccess(false);
-
-        // Check that app is restricted when not idle but power-save is on
-        setAppIdle(false);
-        assertBackgroundNetworkAccess(true);
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-        // Use setBatterySaverMode API to leave power-save mode instead of plugging in charger
-        setBatterySaverMode(false);
-        turnBatteryOff();
-        assertBackgroundNetworkAccess(true);
-
-        // And when no longer charging, it still has network access, since it's not idle
-        turnBatteryOn();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @Test
-    public void testAppIdleNetworkAccess_idleWhitelisted() throws Exception {
-        setAppIdle(true);
-        assertAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-
-        addAppIdleWhitelist(mUid);
-        assertBackgroundNetworkAccess(true);
-
-        removeAppIdleWhitelist(mUid);
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure whitelisting a random app doesn't affect the tested app.
-        addAppIdleWhitelist(mUid + 1);
-        assertBackgroundNetworkAccess(false);
-        removeAppIdleWhitelist(mUid + 1);
-    }
-
-    @Test
-    public void testAppIdle_toast() throws Exception {
-        setAppIdle(true);
-        assertAppIdle(true);
-        assertEquals("Shown", showToast());
-        assertAppIdle(true);
-        // Wait for a couple of seconds for the toast to actually be shown
-        SystemClock.sleep(2000);
-        assertAppIdle(true);
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
deleted file mode 100644
index 0d7365f..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractBatterySaverModeTestCase.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered Battery Saver Mode tests.
- */
-@RequiredProperties({BATTERY_SAVER_MODE})
-abstract class AbstractBatterySaverModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        setBatterySaverMode(false);
-
-        registerBroadcastReceiver();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-
-        setBatterySaverMode(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_enabled() throws Exception {
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground app doesn't lose access upon Battery Saver.
-        setBatterySaverMode(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        setBatterySaverMode(true);
-        assertTopNetworkAccess(true);
-
-        // Although it should not have access while the screen is off.
-        turnScreenOff();
-        assertBackgroundNetworkAccess(false);
-        turnScreenOn();
-        assertTopNetworkAccess(true);
-
-        // Goes back to background state.
-        finishActivity();
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground service doesn't lose access upon enabling Battery Saver.
-        setBatterySaverMode(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        setBatterySaverMode(true);
-        assertForegroundServiceNetworkAccess();
-        stopForegroundService();
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(true);
-
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(true);
-
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_disabled() throws Exception {
-        assertBackgroundNetworkAccess(true);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(true);
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java
deleted file mode 100644
index 8a3e790..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.net.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for default, always-on network restrictions.
- */
-abstract class AbstractDefaultRestrictionsTest extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-
-        registerBroadcastReceiver();
-        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-    }
-
-    @Test
-    public void testFgsNetworkAccess() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-        assertNetworkAccess(false, null);
-
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-    }
-
-    @Test
-    public void testActivityNetworkAccess() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-        assertNetworkAccess(false, null);
-
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-        assertNetworkAccess(false, null);
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        assertNetworkAccess(true, null);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-        assertNetworkAccess(false, null);
-
-        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-        assertNetworkAccess(true, null);
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
deleted file mode 100644
index b037953..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDozeModeTestCase.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-
-import static com.android.cts.net.hostside.Property.DOZE_MODE;
-import static com.android.cts.net.hostside.Property.NOT_LOW_RAM_DEVICE;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Base class for metered and non-metered Doze Mode tests.
- */
-@RequiredProperties({DOZE_MODE})
-abstract class AbstractDozeModeTestCase extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        setDozeMode(false);
-
-        registerBroadcastReceiver();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-
-        setDozeMode(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_enabled() throws Exception {
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground service doesn't lose network access upon enabling doze.
-        setDozeMode(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        setDozeMode(true);
-        assertForegroundServiceNetworkAccess();
-        stopForegroundService();
-        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_whitelisted() throws Exception {
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(true);
-
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-        assertBackgroundNetworkAccess(false);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testBackgroundNetworkAccess_disabled() throws Exception {
-        assertBackgroundNetworkAccess(true);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @RequiredProperties({NOT_LOW_RAM_DEVICE})
-    @Test
-    public void testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction()
-            throws Exception {
-        setPendingIntentAllowlistDuration(NETWORK_TIMEOUT_MS);
-        try {
-            registerNotificationListenerService();
-            setDozeMode(true);
-            assertBackgroundNetworkAccess(false);
-
-            testNotification(4, NOTIFICATION_TYPE_CONTENT);
-            testNotification(8, NOTIFICATION_TYPE_DELETE);
-            testNotification(15, NOTIFICATION_TYPE_FULL_SCREEN);
-            testNotification(16, NOTIFICATION_TYPE_BUNDLE);
-            testNotification(23, NOTIFICATION_TYPE_ACTION);
-            testNotification(42, NOTIFICATION_TYPE_ACTION_BUNDLE);
-            testNotification(108, NOTIFICATION_TYPE_ACTION_REMOTE_INPUT);
-        } finally {
-            resetDeviceIdleSettings();
-        }
-    }
-
-    private void testNotification(int id, String type) throws Exception {
-        sendNotification(id, type);
-        assertBackgroundNetworkAccess(true);
-        if (type.equals(NOTIFICATION_TYPE_ACTION)) {
-            // Make sure access is disabled after it expires. Since this check considerably slows
-            // downs the CTS tests, do it just once.
-            SystemClock.sleep(NETWORK_TIMEOUT_MS);
-            assertBackgroundNetworkAccess(false);
-        }
-    }
-
-    // Must override so it only tests foreground service - once an app goes to foreground, device
-    // leaves Doze Mode.
-    @Override
-    protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception {
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        stopForegroundService();
-        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java
deleted file mode 100644
index 7cac2af..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.DOZE_MODE;
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class AbstractExpeditedJobTest extends AbstractRestrictBackgroundNetworkTestCase {
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-        resetDeviceState();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-        resetDeviceState();
-    }
-
-    private void resetDeviceState() throws Exception {
-        resetBatteryState();
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setAppIdle(false);
-        setDozeMode(false);
-    }
-
-    @Test
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    public void testNetworkAccess_batterySaverMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-    public void testNetworkAccess_dataSaverMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setRestrictBackground(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNoNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({APP_STANDBY_MODE})
-    public void testNetworkAccess_appIdleState() throws Exception {
-        turnBatteryOn();
-        setAppIdle(false);
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setAppIdle(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DOZE_MODE})
-    public void testNetworkAccess_dozeMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
-    public void testNetworkAccess_dataAndBatterySaverMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setRestrictBackground(true);
-        setBatterySaverMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNoNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DOZE_MODE, DATA_SAVER_MODE, METERED_NETWORK})
-    public void testNetworkAccess_dozeAndDataSaverMode() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setRestrictBackground(true);
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNoNetworkAccess();
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK, DOZE_MODE,
-            APP_STANDBY_MODE})
-    public void testNetworkAccess_allRestrictionsEnabled() throws Exception {
-        assertBackgroundNetworkAccess(true);
-        assertExpeditedJobHasNetworkAccess();
-
-        setRestrictBackground(true);
-        setBatterySaverMode(true);
-        setAppIdle(true);
-        setDozeMode(true);
-        assertBackgroundNetworkAccess(false);
-        assertExpeditedJobHasNoNetworkAccess();
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
deleted file mode 100644
index 2ca8832..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ /dev/null
@@ -1,1125 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP;
-import static android.app.job.JobScheduler.RESULT_SUCCESS;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.os.BatteryManager.BATTERY_PLUGGED_ANY;
-
-import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.forceRunJob;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getContext;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getInstrumentation;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackgroundInternal;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.annotation.NonNull;
-import android.app.Instrumentation;
-import android.app.NotificationManager;
-import android.app.job.JobInfo;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo.DetailedState;
-import android.net.NetworkInfo.State;
-import android.net.NetworkRequest;
-import android.os.BatteryManager;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.PowerManager;
-import android.os.RemoteCallback;
-import android.os.SystemClock;
-import android.provider.DeviceConfig;
-import android.service.notification.NotificationListenerService;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.Nullable;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.compatibility.common.util.AmUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-import com.android.compatibility.common.util.DeviceConfigStateHelper;
-import com.android.compatibility.common.util.ThrowingRunnable;
-import com.android.modules.utils.build.SdkLevel;
-
-import org.junit.Rule;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Predicate;
-
-/**
- * Superclass for tests related to background network restrictions.
- */
-@RunWith(NetworkPolicyTestRunner.class)
-public abstract class AbstractRestrictBackgroundNetworkTestCase {
-    public static final String TAG = "RestrictBackgroundNetworkTests";
-
-    protected static final String TEST_PKG = "com.android.cts.net.hostside";
-    protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
-    // TODO(b/321797685): Configure it via device-config once it is available.
-    protected static final long PROCESS_STATE_TRANSITION_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
-
-    private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity";
-    private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService";
-    private static final String TEST_APP2_JOB_SERVICE_CLASS = TEST_APP2_PKG + ".MyJobService";
-
-    private static final ComponentName TEST_JOB_COMPONENT = new ComponentName(
-            TEST_APP2_PKG, TEST_APP2_JOB_SERVICE_CLASS);
-    private static final int TEST_JOB_ID = 7357437;
-
-    private static final int SLEEP_TIME_SEC = 1;
-
-    // Constants below must match values defined on app2's Common.java
-    private static final String MANIFEST_RECEIVER = "ManifestReceiver";
-    private static final String DYNAMIC_RECEIVER = "DynamicReceiver";
-    private static final String ACTION_FINISH_ACTIVITY =
-            "com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY";
-    private static final String ACTION_FINISH_JOB =
-            "com.android.cts.net.hostside.app2.action.FINISH_JOB";
-    // Copied from com.android.server.net.NetworkPolicyManagerService class
-    private static final String ACTION_SNOOZE_WARNING =
-            "com.android.server.net.action.SNOOZE_WARNING";
-
-    private static final String ACTION_RECEIVER_READY =
-            "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
-    static final String ACTION_SHOW_TOAST =
-            "com.android.cts.net.hostside.app2.action.SHOW_TOAST";
-
-    protected static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
-    protected static final String NOTIFICATION_TYPE_DELETE = "DELETE";
-    protected static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
-    protected static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
-    protected static final String NOTIFICATION_TYPE_ACTION = "ACTION";
-    protected static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
-    protected static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
-
-    private static final String NETWORK_STATUS_SEPARATOR = "\\|";
-    private static final int SECOND_IN_MS = 1000;
-    static final int NETWORK_TIMEOUT_MS = 15 * SECOND_IN_MS;
-
-    private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
-    private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
-
-    private static final String EMPTY_STRING = "";
-
-    protected static final int TYPE_COMPONENT_ACTIVTIY = 0;
-    protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
-    protected static final int TYPE_EXPEDITED_JOB = 2;
-
-    private static final int BATTERY_STATE_TIMEOUT_MS = 5000;
-    private static final int BATTERY_STATE_CHECK_INTERVAL_MS = 500;
-
-    private static final int ACTIVITY_NETWORK_STATE_TIMEOUT_MS = 10_000;
-    private static final int JOB_NETWORK_STATE_TIMEOUT_MS = 10_000;
-    private static final int LAUNCH_ACTIVITY_TIMEOUT_MS = 10_000;
-
-    // Must be higher than NETWORK_TIMEOUT_MS
-    private static final int ORDERED_BROADCAST_TIMEOUT_MS = NETWORK_TIMEOUT_MS * 4;
-
-    private static final IntentFilter BATTERY_CHANGED_FILTER =
-            new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
-
-    protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
-
-    private static final long BROADCAST_TIMEOUT_MS = 5_000;
-
-    protected Context mContext;
-    protected Instrumentation mInstrumentation;
-    protected ConnectivityManager mCm;
-    protected int mUid;
-    private int mMyUid;
-    private MyServiceClient mServiceClient;
-    private DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper;
-    private PowerManager mPowerManager;
-    private PowerManager.WakeLock mLock;
-
-    @Rule
-    public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule())
-            .around(new MeterednessConfigurationRule());
-
-    protected void setUp() throws Exception {
-        mInstrumentation = getInstrumentation();
-        mContext = getContext();
-        mCm = getConnectivityManager();
-        mDeviceIdleDeviceConfigStateHelper =
-                new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_DEVICE_IDLE);
-        mUid = getUid(TEST_APP2_PKG);
-        mMyUid = getUid(mContext.getPackageName());
-        mServiceClient = new MyServiceClient(mContext);
-
-        final Bundle args = InstrumentationRegistry.getArguments();
-        final int bindPriorityFlags;
-        if (Boolean.valueOf(args.getString(ARG_WAIVE_BIND_PRIORITY, "false"))) {
-            bindPriorityFlags = Context.BIND_WAIVE_PRIORITY;
-        } else {
-            bindPriorityFlags = Context.BIND_NOT_FOREGROUND;
-        }
-        mServiceClient.bind(bindPriorityFlags);
-
-        mPowerManager = mContext.getSystemService(PowerManager.class);
-        executeShellCommand("cmd netpolicy start-watching " + mUid);
-        // Some of the test cases assume that Data saver mode is initially disabled, which might not
-        // always be the case. Therefore, explicitly disable it before running the tests.
-        // Invoke setRestrictBackgroundInternal() directly instead of going through
-        // setRestrictBackground(), as some devices do not fully support the Data saver mode but
-        // still have certain parts of it enabled by default.
-        setRestrictBackgroundInternal(false);
-        setAppIdle(false);
-        mLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
-
-        Log.i(TAG, "Apps status:\n"
-                + "\ttest app: uid=" + mMyUid + ", state=" + getProcessStateByUid(mMyUid) + "\n"
-                + "\tapp2: uid=" + mUid + ", state=" + getProcessStateByUid(mUid));
-    }
-
-    protected void tearDown() throws Exception {
-        executeShellCommand("cmd netpolicy stop-watching");
-        mServiceClient.unbind();
-        final PowerManager.WakeLock lock = mLock;
-        if (null != lock && lock.isHeld()) lock.release();
-    }
-
-    /**
-     * Check if the feature blocking network for top_sleeping and lower priority proc-states is
-     * enabled. This is a manual check because the feature flag infrastructure may not be available
-     * in all the branches that will get this code.
-     * TODO: b/322115994 - Use @RequiresFlagsEnabled with
-     * Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE once the tests are moved to cts.
-     */
-    protected boolean isNetworkBlockedForTopSleepingAndAbove() {
-        if (!SdkLevel.isAtLeastV()) {
-            return false;
-        }
-        final String output = executeShellCommand("device_config get backstage_power"
-                + " com.android.server.net.network_blocked_for_top_sleeping_and_above");
-        return Boolean.parseBoolean(output);
-    }
-
-    protected int getUid(String packageName) throws Exception {
-        return mContext.getPackageManager().getPackageUid(packageName, 0);
-    }
-
-    protected void assertRestrictBackgroundChangedReceived(int expectedCount) throws Exception {
-        assertRestrictBackgroundChangedReceived(DYNAMIC_RECEIVER, expectedCount);
-        assertRestrictBackgroundChangedReceived(MANIFEST_RECEIVER, 0);
-    }
-
-    protected void assertRestrictBackgroundChangedReceived(String receiverName, int expectedCount)
-            throws Exception {
-        int attempts = 0;
-        int count = 0;
-        final int maxAttempts = 5;
-        do {
-            attempts++;
-            count = getNumberBroadcastsReceived(receiverName, ACTION_RESTRICT_BACKGROUND_CHANGED);
-            assertFalse("Expected count " + expectedCount + " but actual is " + count,
-                    count > expectedCount);
-            if (count == expectedCount) {
-                break;
-            }
-            Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
-                    + attempts + " attempts; sleeping "
-                    + SLEEP_TIME_SEC + " seconds before trying again");
-            // No sleep after the last turn
-            if (attempts <= maxAttempts) {
-                SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
-            }
-        } while (attempts <= maxAttempts);
-        assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
-                + maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
-    }
-
-    protected void assertSnoozeWarningNotReceived() throws Exception {
-        // Wait for a while to take broadcast queue delays into account
-        SystemClock.sleep(BROADCAST_TIMEOUT_MS);
-        assertEquals(0, getNumberBroadcastsReceived(DYNAMIC_RECEIVER, ACTION_SNOOZE_WARNING));
-    }
-
-    protected String sendOrderedBroadcast(Intent intent) throws Exception {
-        return sendOrderedBroadcast(intent, ORDERED_BROADCAST_TIMEOUT_MS);
-    }
-
-    protected String sendOrderedBroadcast(Intent intent, int timeoutMs) throws Exception {
-        final LinkedBlockingQueue<String> result = new LinkedBlockingQueue<>(1);
-        Log.d(TAG, "Sending ordered broadcast: " + intent);
-        mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
-
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                final String resultData = getResultData();
-                if (resultData == null) {
-                    Log.e(TAG, "Received null data from ordered intent");
-                    // Offer an empty string so that the code waiting for the result can return.
-                    result.offer(EMPTY_STRING);
-                    return;
-                }
-                result.offer(resultData);
-            }
-        }, null, 0, null, null);
-
-        final String resultData = result.poll(timeoutMs, TimeUnit.MILLISECONDS);
-        Log.d(TAG, "Ordered broadcast response after " + timeoutMs + "ms: " + resultData );
-        return resultData;
-    }
-
-    protected int getNumberBroadcastsReceived(String receiverName, String action) throws Exception {
-        return mServiceClient.getCounters(receiverName, action);
-    }
-
-    protected void assertRestrictBackgroundStatus(int expectedStatus) throws Exception {
-        final String status = mServiceClient.getRestrictBackgroundStatus();
-        assertNotNull("didn't get API status from app2", status);
-        assertEquals(restrictBackgroundValueToString(expectedStatus),
-                restrictBackgroundValueToString(Integer.parseInt(status)));
-    }
-
-    /**
-     * @deprecated The definition of "background" can be ambiguous. Use separate calls to
-     * {@link #assertProcessStateBelow(int)} with
-     * {@link #assertNetworkAccess(boolean, boolean, String)} to be explicit, instead.
-     */
-    @Deprecated
-    protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
-        assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-        assertNetworkAccess(expectAllowed, false, null);
-    }
-
-    protected void assertTopNetworkAccess(boolean expectAllowed) throws Exception {
-        assertTopState();
-        assertNetworkAccess(expectAllowed, true /* needScreenOn */);
-    }
-
-    protected void assertForegroundServiceNetworkAccess() throws Exception {
-        assertForegroundServiceState();
-        assertNetworkAccess(true /* expectAvailable */, false /* needScreenOn */);
-    }
-
-    /**
-     * Asserts that an app always have access while on foreground or running a foreground service.
-     *
-     * <p>This method will launch an activity, a foreground service to make
-     * the assertion, but will finish the activity / stop the service afterwards.
-     */
-    protected void assertsForegroundAlwaysHasNetworkAccess() throws Exception{
-        // Checks foreground first.
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        finishActivity();
-
-        // Then foreground service
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        stopForegroundService();
-    }
-
-    protected void assertExpeditedJobHasNetworkAccess() throws Exception {
-        launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB);
-        finishExpeditedJob();
-    }
-
-    protected void assertExpeditedJobHasNoNetworkAccess() throws Exception {
-        launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB, false);
-        finishExpeditedJob();
-    }
-
-    /**
-     * Asserts that the process state of the test app is below, in priority, to the given
-     * {@link android.app.ActivityManager.ProcessState}.
-     */
-    protected final void assertProcessStateBelow(int processState) throws Exception {
-        assertProcessState(ps -> ps.state > processState, null);
-    }
-
-    protected final void assertTopState() throws Exception {
-        assertProcessState(ps -> ps.state == PROCESS_STATE_TOP, () -> turnScreenOn());
-    }
-
-    protected final void assertForegroundServiceState() throws Exception {
-        assertProcessState(ps -> ps.state == PROCESS_STATE_FOREGROUND_SERVICE, null);
-    }
-
-    private void assertProcessState(Predicate<ProcessState> statePredicate,
-            ThrowingRunnable onRetry) throws Exception {
-        final int maxTries = 30;
-        ProcessState state = null;
-        for (int i = 1; i <= maxTries; i++) {
-            if (onRetry != null) {
-                onRetry.run();
-            }
-            state = getProcessStateByUid(mUid);
-            Log.v(TAG, "assertProcessState(): status for app2 (" + mUid + ") on attempt #" + i
-                    + ": " + state);
-            if (statePredicate.test(state)) {
-                return;
-            }
-            Log.i(TAG, "App not in desired process state on attempt #" + i
-                    + "; sleeping 1s before trying again");
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail("App2 (" + mUid + ") is not in the desired process state after " + maxTries
-                + " attempts: " + state);
-    }
-
-    /**
-     * Asserts whether the active network is available or not. If the network is unavailable, also
-     * checks whether it is blocked by the expected error.
-     *
-     * @param expectAllowed expect background network access to be allowed or not.
-     * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
-     *                                 meaningful only when the {@code expectAllowed} is 'false'.
-     *                                 Throws an IllegalArgumentException when {@code expectAllowed}
-     *                                 is true and this parameter is not null. When the
-     *                                 {@code expectAllowed} is 'false' and this parameter is null,
-     *                                 this function does not compare error type of the networking
-     *                                 access failure.
-     */
-    protected void assertNetworkAccess(boolean expectAllowed, String expectedUnavailableError)
-            throws Exception {
-        if (expectAllowed && expectedUnavailableError != null) {
-            throw new IllegalArgumentException("expectedUnavailableError is not null");
-        }
-        assertNetworkAccess(expectAllowed, false, expectedUnavailableError);
-    }
-
-    /**
-     * Asserts whether the active network is available or not.
-     */
-    private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
-            throws Exception {
-        assertNetworkAccess(expectAvailable, needScreenOn, null);
-    }
-
-    private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn,
-            @Nullable final String expectedUnavailableError) throws Exception {
-        final int maxTries = 5;
-        String error = null;
-        int timeoutMs = 500;
-
-        for (int i = 1; i <= maxTries; i++) {
-            error = checkNetworkAccess(expectAvailable, expectedUnavailableError);
-
-            if (error == null) return;
-
-            // TODO: ideally, it should retry only when it cannot connect to an external site,
-            // or no retry at all! But, currently, the initial change fails almost always on
-            // battery saver tests because the netd changes are made asynchronously.
-            // Once b/27803922 is fixed, this retry mechanism should be revisited.
-
-            Log.w(TAG, "Network status didn't match for expectAvailable=" + expectAvailable
-                    + " on attempt #" + i + ": " + error + "\n"
-                    + "Sleeping " + timeoutMs + "ms before trying again");
-            if (needScreenOn) {
-                turnScreenOn();
-            }
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(timeoutMs);
-            }
-            // Exponential back-off.
-            timeoutMs = Math.min(timeoutMs*2, NETWORK_TIMEOUT_MS);
-        }
-        fail("Invalid state for " + mUid + "; expectAvailable=" + expectAvailable + " after "
-                + maxTries + " attempts.\nLast error: " + error);
-    }
-
-    /**
-     * Asserts whether the network is blocked by accessing bpf maps if command-line tool supports.
-     */
-    void assertNetworkAccessBlockedByBpf(boolean expectBlocked, int uid, boolean metered) {
-        final String result;
-        try {
-            result = executeShellCommand(
-                    "cmd network_stack is-uid-networking-blocked " + uid + " " + metered);
-        } catch (AssertionError e) {
-            // If NetworkStack is too old to support this command, ignore and continue
-            // this test to verify other parts.
-            if (e.getMessage().contains("No shell command implementation.")) {
-                return;
-            }
-            throw e;
-        }
-
-        // Tethering module is too old.
-        if (result.contains("API is unsupported")) {
-            return;
-        }
-
-        assertEquals(expectBlocked, parseBooleanOrThrow(result.trim()));
-    }
-
-    /**
-     * Similar to {@link Boolean#parseBoolean} but throws when the input
-     * is unexpected instead of returning false.
-     */
-    private static boolean parseBooleanOrThrow(@NonNull String s) {
-        // Don't use Boolean.parseBoolean
-        if ("true".equalsIgnoreCase(s)) return true;
-        if ("false".equalsIgnoreCase(s)) return false;
-        throw new IllegalArgumentException("Unexpected: " + s);
-    }
-
-    /**
-     * Checks whether the network is available as expected.
-     *
-     * @return error message with the mismatch (or empty if assertion passed).
-     */
-    private String checkNetworkAccess(boolean expectAvailable,
-            @Nullable final String expectedUnavailableError) throws Exception {
-        final String resultData = mServiceClient.checkNetworkStatus();
-        return checkForAvailabilityInResultData(resultData, expectAvailable,
-                expectedUnavailableError);
-    }
-
-    private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable,
-            @Nullable final String expectedUnavailableError) {
-        if (resultData == null) {
-            assertNotNull("Network status from app2 is null", resultData);
-        }
-        // Network status format is described on MyBroadcastReceiver.checkNetworkStatus()
-        final String[] parts = resultData.split(NETWORK_STATUS_SEPARATOR);
-        assertEquals("Wrong network status: " + resultData, 5, parts.length);
-        final State state = parts[0].equals("null") ? null : State.valueOf(parts[0]);
-        final DetailedState detailedState = parts[1].equals("null")
-                ? null : DetailedState.valueOf(parts[1]);
-        final boolean connected = Boolean.valueOf(parts[2]);
-        final String connectionCheckDetails = parts[3];
-        final String networkInfo = parts[4];
-
-        final StringBuilder errors = new StringBuilder();
-        final State expectedState;
-        final DetailedState expectedDetailedState;
-        if (expectAvailable) {
-            expectedState = State.CONNECTED;
-            expectedDetailedState = DetailedState.CONNECTED;
-        } else {
-            expectedState = State.DISCONNECTED;
-            expectedDetailedState = DetailedState.BLOCKED;
-        }
-
-        if (expectAvailable != connected) {
-            errors.append(String.format("External site connection failed: expected %s, got %s\n",
-                    expectAvailable, connected));
-        }
-        if (expectedState != state || expectedDetailedState != detailedState) {
-            errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
-                    expectedState, expectedDetailedState, state, detailedState));
-        } else if (!expectAvailable && (expectedUnavailableError != null)
-                 && !connectionCheckDetails.contains(expectedUnavailableError)) {
-            errors.append("Connection unavailable reason mismatch: expected "
-                     + expectedUnavailableError + "\n");
-        }
-
-        if (errors.length() > 0) {
-            errors.append("\tnetworkInfo: " + networkInfo + "\n");
-            errors.append("\tconnectionCheckDetails: " + connectionCheckDetails + "\n");
-        }
-        return errors.length() == 0 ? null : errors.toString();
-    }
-
-    /**
-     * Runs a Shell command which is not expected to generate output.
-     */
-    protected void executeSilentShellCommand(String command) {
-        final String result = executeShellCommand(command);
-        assertTrue("Command '" + command + "' failed: " + result, result.trim().isEmpty());
-    }
-
-    /**
-     * Asserts the result of a command, wait and re-running it a couple times if necessary.
-     */
-    protected void assertDelayedShellCommand(String command, final String expectedResult)
-            throws Exception {
-        assertDelayedShellCommand(command, 5, 1, expectedResult);
-    }
-
-    protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
-            final String expectedResult) throws Exception {
-        assertDelayedShellCommand(command, maxTries, napTimeSeconds, new ExpectResultChecker() {
-
-            @Override
-            public boolean isExpected(String result) {
-                return expectedResult.equals(result);
-            }
-
-            @Override
-            public String getExpected() {
-                return expectedResult;
-            }
-        });
-    }
-
-    protected void assertDelayedShellCommand(String command, int maxTries, int napTimeSeconds,
-            ExpectResultChecker checker) throws Exception {
-        String result = "";
-        for (int i = 1; i <= maxTries; i++) {
-            result = executeShellCommand(command).trim();
-            if (checker.isExpected(result)) return;
-            Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
-                    + checker.getExpected() + "' on attempt #" + i
-                    + "; sleeping " + napTimeSeconds + "s before trying again");
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
-            }
-        }
-        fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
-                + maxTries
-                + " attempts. Last result: '" + result + "'");
-    }
-
-    protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy add restrict-background-whitelist " + uid);
-        assertRestrictBackgroundWhitelist(uid, true);
-        // UID policies live by the Highlander rule: "There can be only one".
-        // Hence, if app is whitelisted, it should not be blacklisted.
-        assertRestrictBackgroundBlacklist(uid, false);
-    }
-
-    protected void removeRestrictBackgroundWhitelist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy remove restrict-background-whitelist " + uid);
-        assertRestrictBackgroundWhitelist(uid, false);
-    }
-
-    protected void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
-        assertRestrictBackground("restrict-background-whitelist", uid, expected);
-    }
-
-    protected void addRestrictBackgroundBlacklist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy add restrict-background-blacklist " + uid);
-        assertRestrictBackgroundBlacklist(uid, true);
-        // UID policies live by the Highlander rule: "There can be only one".
-        // Hence, if app is blacklisted, it should not be whitelisted.
-        assertRestrictBackgroundWhitelist(uid, false);
-    }
-
-    protected void removeRestrictBackgroundBlacklist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy remove restrict-background-blacklist " + uid);
-        assertRestrictBackgroundBlacklist(uid, false);
-    }
-
-    protected void assertRestrictBackgroundBlacklist(int uid, boolean expected) throws Exception {
-        assertRestrictBackground("restrict-background-blacklist", uid, expected);
-    }
-
-    protected void addAppIdleWhitelist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy add app-idle-whitelist " + uid);
-        assertAppIdleWhitelist(uid, true);
-    }
-
-    protected void removeAppIdleWhitelist(int uid) throws Exception {
-        executeShellCommand("cmd netpolicy remove app-idle-whitelist " + uid);
-        assertAppIdleWhitelist(uid, false);
-    }
-
-    protected void assertAppIdleWhitelist(int uid, boolean expected) throws Exception {
-        assertRestrictBackground("app-idle-whitelist", uid, expected);
-    }
-
-    private void assertRestrictBackground(String list, int uid, boolean expected) throws Exception {
-        final int maxTries = 5;
-        boolean actual = false;
-        final String expectedUid = Integer.toString(uid);
-        String uids = "";
-        for (int i = 1; i <= maxTries; i++) {
-            final String output =
-                    executeShellCommand("cmd netpolicy list " + list);
-            uids = output.split(":")[1];
-            for (String candidate : uids.split(" ")) {
-                actual = candidate.trim().equals(expectedUid);
-                if (expected == actual) {
-                    return;
-                }
-            }
-            Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
-                    + expected + ", got " + actual + "); sleeping 1s before polling again");
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
-                + ". Full list: " + uids);
-    }
-
-    protected void addTempPowerSaveModeWhitelist(String packageName, long duration)
-            throws Exception {
-        Log.i(TAG, "Adding pkg " + packageName + " to temp-power-save-mode whitelist");
-        executeShellCommand("dumpsys deviceidle tempwhitelist -d " + duration + " " + packageName);
-    }
-
-    protected void assertPowerSaveModeWhitelist(String packageName, boolean expected)
-            throws Exception {
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        assertDelayedShellCommand("dumpsys deviceidle whitelist =" + packageName,
-                Boolean.toString(expected));
-    }
-
-    protected void addPowerSaveModeWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        executeShellCommand("dumpsys deviceidle whitelist +" + packageName);
-        assertPowerSaveModeWhitelist(packageName, true);
-    }
-
-    protected void removePowerSaveModeWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Removing package " + packageName + " from power-save-mode whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        executeShellCommand("dumpsys deviceidle whitelist -" + packageName);
-        assertPowerSaveModeWhitelist(packageName, false);
-    }
-
-    protected void assertPowerSaveModeExceptIdleWhitelist(String packageName, boolean expected)
-            throws Exception {
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        assertDelayedShellCommand("dumpsys deviceidle except-idle-whitelist =" + packageName,
-                Boolean.toString(expected));
-    }
-
-    protected void addPowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Adding package " + packageName + " to power-save-mode-except-idle whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        executeShellCommand("dumpsys deviceidle except-idle-whitelist +" + packageName);
-        assertPowerSaveModeExceptIdleWhitelist(packageName, true);
-    }
-
-    protected void removePowerSaveModeExceptIdleWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Removing package " + packageName
-                + " from power-save-mode-except-idle whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        executeShellCommand("dumpsys deviceidle except-idle-whitelist reset");
-        assertPowerSaveModeExceptIdleWhitelist(packageName, false);
-    }
-
-    protected void turnBatteryOn() throws Exception {
-        executeSilentShellCommand("cmd battery unplug");
-        executeSilentShellCommand("cmd battery set status "
-                + BatteryManager.BATTERY_STATUS_DISCHARGING);
-        assertBatteryState(false);
-    }
-
-    protected void turnBatteryOff() throws Exception {
-        executeSilentShellCommand("cmd battery set ac " + BATTERY_PLUGGED_ANY);
-        executeSilentShellCommand("cmd battery set level 100");
-        executeSilentShellCommand("cmd battery set status "
-                + BatteryManager.BATTERY_STATUS_CHARGING);
-        assertBatteryState(true);
-    }
-
-    protected void resetBatteryState() {
-        BatteryUtils.runDumpsysBatteryReset();
-    }
-
-    private void assertBatteryState(boolean pluggedIn) throws Exception {
-        final long endTime = SystemClock.elapsedRealtime() + BATTERY_STATE_TIMEOUT_MS;
-        while (isDevicePluggedIn() != pluggedIn && SystemClock.elapsedRealtime() <= endTime) {
-            Thread.sleep(BATTERY_STATE_CHECK_INTERVAL_MS);
-        }
-        if (isDevicePluggedIn() != pluggedIn) {
-            fail("Timed out waiting for the plugged-in state to change,"
-                    + " expected pluggedIn: " + pluggedIn);
-        }
-    }
-
-    private boolean isDevicePluggedIn() {
-        final Intent batteryIntent = mContext.registerReceiver(null, BATTERY_CHANGED_FILTER);
-        return batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0;
-    }
-
-    protected void turnScreenOff() throws Exception {
-        if (!mLock.isHeld()) mLock.acquire();
-        executeSilentShellCommand("input keyevent KEYCODE_SLEEP");
-    }
-
-    protected void turnScreenOn() throws Exception {
-        executeSilentShellCommand("input keyevent KEYCODE_WAKEUP");
-        if (mLock.isHeld()) mLock.release();
-        executeSilentShellCommand("wm dismiss-keyguard");
-    }
-
-    protected void setBatterySaverMode(boolean enabled) throws Exception {
-        if (!isBatterySaverSupported()) {
-            return;
-        }
-        Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
-        if (enabled) {
-            turnBatteryOn();
-            AmUtils.waitForBroadcastBarrier();
-            executeSilentShellCommand("cmd power set-mode 1");
-        } else {
-            executeSilentShellCommand("cmd power set-mode 0");
-            turnBatteryOff();
-            AmUtils.waitForBroadcastBarrier();
-        }
-    }
-
-    protected void setDozeMode(boolean enabled) throws Exception {
-        if (!isDozeModeSupported()) {
-            return;
-        }
-
-        Log.i(TAG, "Setting Doze Mode to " + enabled);
-        if (enabled) {
-            turnBatteryOn();
-            turnScreenOff();
-            executeShellCommand("dumpsys deviceidle force-idle deep");
-        } else {
-            turnScreenOn();
-            turnBatteryOff();
-            executeShellCommand("dumpsys deviceidle unforce");
-        }
-        assertDozeMode(enabled);
-    }
-
-    protected void assertDozeMode(boolean enabled) throws Exception {
-        assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
-    }
-
-    protected void setAppIdle(boolean isIdle) throws Exception {
-        setAppIdleNoAssert(isIdle);
-        assertAppIdle(isIdle);
-    }
-
-    protected void setAppIdleNoAssert(boolean isIdle) throws Exception {
-        if (!isAppStandbySupported()) {
-            return;
-        }
-        Log.i(TAG, "Setting app idle to " + isIdle);
-        final String bucketName = isIdle ? "rare" : "active";
-        executeSilentShellCommand("am set-standby-bucket " + TEST_APP2_PKG + " " + bucketName);
-    }
-
-    protected void assertAppIdle(boolean isIdle) throws Exception {
-        try {
-            assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
-                    30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + isIdle);
-        } catch (Throwable e) {
-            throw e;
-        }
-    }
-
-    /**
-     * Starts a service that will register a broadcast receiver to receive
-     * {@code RESTRICT_BACKGROUND_CHANGE} intents.
-     * <p>
-     * The service must run in a separate app because otherwise it would be killed every time
-     * {@link #runDeviceTests(String, String)} is executed.
-     */
-    protected void registerBroadcastReceiver() throws Exception {
-        mServiceClient.registerBroadcastReceiver();
-
-        final Intent intent = new Intent(ACTION_RECEIVER_READY)
-                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        // Wait until receiver is ready.
-        final int maxTries = 10;
-        for (int i = 1; i <= maxTries; i++) {
-            final String message = sendOrderedBroadcast(intent, SECOND_IN_MS * 4);
-            Log.d(TAG, "app2 receiver acked: " + message);
-            if (message != null) {
-                return;
-            }
-            Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
-            // No sleep after the last turn
-            if (i < maxTries) {
-                SystemClock.sleep(SECOND_IN_MS);
-            }
-        }
-        fail("app2 receiver is not ready in " + mUid);
-    }
-
-    protected void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
-            throws Exception {
-        Log.i(TAG, "Registering network callback for request: " + request);
-        mServiceClient.registerNetworkCallback(request, cb);
-    }
-
-    protected void unregisterNetworkCallback() throws Exception {
-        mServiceClient.unregisterNetworkCallback();
-    }
-
-    /**
-     * Registers a {@link NotificationListenerService} implementation that will execute the
-     * notification actions right after the notification is sent.
-     */
-    protected void registerNotificationListenerService() throws Exception {
-        executeShellCommand("cmd notification allow_listener "
-                + MyNotificationListenerService.getId());
-        final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
-        final ComponentName listenerComponent = MyNotificationListenerService.getComponentName();
-        assertTrue(listenerComponent + " has not been granted access",
-                nm.isNotificationListenerAccessGranted(listenerComponent));
-    }
-
-    protected void setPendingIntentAllowlistDuration(long durationMs) {
-        mDeviceIdleDeviceConfigStateHelper.set("notification_allowlist_duration_ms",
-                String.valueOf(durationMs));
-    }
-
-    protected void resetDeviceIdleSettings() {
-        mDeviceIdleDeviceConfigStateHelper.restoreOriginalValues();
-    }
-
-    protected void launchActivity() throws Exception {
-        turnScreenOn();
-        final CountDownLatch latch = new CountDownLatch(1);
-        final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
-        final RemoteCallback callback = new RemoteCallback(result -> latch.countDown());
-        launchIntent.putExtra(Intent.EXTRA_REMOTE_CALLBACK, callback);
-        mContext.startActivity(launchIntent);
-        // There might be a race when app2 is launched but ACTION_FINISH_ACTIVITY has not registered
-        // before test calls finishActivity(). When the issue is happened, there is no way to fix
-        // it, so have a callback design to make sure that the app is launched completely and
-        // ACTION_FINISH_ACTIVITY will be registered before leaving this method.
-        if (!latch.await(LAUNCH_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-            fail("Timed out waiting for launching activity");
-        }
-    }
-
-    protected void launchComponentAndAssertNetworkAccess(int type) throws Exception {
-        launchComponentAndAssertNetworkAccess(type, true);
-    }
-
-    protected void launchComponentAndAssertNetworkAccess(int type, boolean expectAvailable)
-            throws Exception {
-        if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
-            startForegroundService();
-            assertForegroundServiceNetworkAccess();
-            return;
-        } else if (type == TYPE_COMPONENT_ACTIVTIY) {
-            turnScreenOn();
-            final CountDownLatch latch = new CountDownLatch(1);
-            final Intent launchIntent = getIntentForComponent(type);
-            final Bundle extras = new Bundle();
-            final ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
-            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
-            extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
-            launchIntent.putExtras(extras);
-            mContext.startActivity(launchIntent);
-            if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                final int resultCode = result.get(0).first;
-                final String resultData = result.get(0).second;
-                if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
-                    final String error = checkForAvailabilityInResultData(
-                            resultData, expectAvailable, null /* expectedUnavailableError */);
-                    if (error != null) {
-                        fail("Network is not available for activity in app2 (" + mUid + "): "
-                                + error);
-                    }
-                } else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) {
-                    Log.d(TAG, resultData);
-                    // App didn't come to foreground when the activity is started, so try again.
-                    assertTopNetworkAccess(true);
-                } else {
-                    fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
-                }
-            } else {
-                fail("Timed out waiting for network availability status from app2's activity ("
-                        + mUid + ")");
-            }
-        } else if (type == TYPE_EXPEDITED_JOB) {
-            final Bundle extras = new Bundle();
-            final ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
-            final CountDownLatch latch = new CountDownLatch(1);
-            extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
-            extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
-            final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT)
-                    .setExpedited(true)
-                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
-                    .setTransientExtras(extras)
-                    .build();
-            assertEquals("Error scheduling " + jobInfo,
-                    RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
-            forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
-            if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                final int resultCode = result.get(0).first;
-                final String resultData = result.get(0).second;
-                if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
-                    final String error = checkForAvailabilityInResultData(
-                            resultData, expectAvailable, null /* expectedUnavailableError */);
-                    if (error != null) {
-                        Log.d(TAG, "Network state is unexpected, checking again. " + error);
-                        // Right now we could end up in an unexpected state if expedited job
-                        // doesn't have network access immediately after starting, so check again.
-                        assertNetworkAccess(expectAvailable, false /* needScreenOn */);
-                    }
-                } else {
-                    fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
-                }
-            } else {
-                fail("Timed out waiting for network availability status from app2's expedited job ("
-                        + mUid + ")");
-            }
-        } else {
-            throw new IllegalArgumentException("Unknown type: " + type);
-        }
-    }
-
-    protected void startActivity() throws Exception {
-        final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_ACTIVTIY);
-        mContext.startActivity(launchIntent);
-    }
-
-    private void startForegroundService() throws Exception {
-        final Intent launchIntent = getIntentForComponent(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        mContext.startForegroundService(launchIntent);
-        assertForegroundServiceState();
-    }
-
-    private Intent getIntentForComponent(int type) {
-        final Intent intent = new Intent();
-        if (type == TYPE_COMPONENT_ACTIVTIY) {
-            intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
-        } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
-            intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
-                    .setFlags(1);
-        } else {
-            fail("Unknown type: " + type);
-        }
-        return intent;
-    }
-
-    protected void stopForegroundService() throws Exception {
-        executeShellCommand(String.format("am startservice -f 2 %s/%s",
-                TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS));
-        // NOTE: cannot assert state because it depends on whether activity was on top before.
-    }
-
-    private Binder getNewNetworkStateObserver(final CountDownLatch latch,
-            final ArrayList<Pair<Integer, String>> result) {
-        return new INetworkStateObserver.Stub() {
-            @Override
-            public void onNetworkStateChecked(int resultCode, String resultData) {
-                result.add(Pair.create(resultCode, resultData));
-                latch.countDown();
-            }
-        };
-    }
-
-    /**
-     * Finishes an activity on app2 so its process is demoted from foreground status.
-     */
-    protected void finishActivity() throws Exception {
-        final Intent intent = new Intent(ACTION_FINISH_ACTIVITY)
-                .setPackage(TEST_APP2_PKG)
-                .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-        sendOrderedBroadcast(intent);
-    }
-
-    /**
-     * Finishes the expedited job on app2 so its process is demoted from foreground status.
-     */
-    private void finishExpeditedJob() throws Exception {
-        final Intent intent = new Intent(ACTION_FINISH_JOB)
-                .setPackage(TEST_APP2_PKG)
-                .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-        sendOrderedBroadcast(intent);
-    }
-
-    protected void sendNotification(int notificationId, String notificationType) throws Exception {
-        Log.d(TAG, "Sending notification broadcast (id=" + notificationId
-                + ", type=" + notificationType);
-        mServiceClient.sendNotification(notificationId, notificationType);
-    }
-
-    protected String showToast() {
-        final Intent intent = new Intent(ACTION_SHOW_TOAST);
-        intent.setPackage(TEST_APP2_PKG);
-        Log.d(TAG, "Sending request to show toast");
-        try {
-            return sendOrderedBroadcast(intent, 3 * SECOND_IN_MS);
-        } catch (Exception e) {
-            return "";
-        }
-    }
-
-    private ProcessState getProcessStateByUid(int uid) throws Exception {
-        return new ProcessState(executeShellCommand("cmd activity get-uid-state " + uid));
-    }
-
-    private static class ProcessState {
-        private final String fullState;
-        final int state;
-
-        ProcessState(String fullState) {
-            this.fullState = fullState;
-            try {
-                this.state = Integer.parseInt(fullState.split(" ")[0]);
-            } catch (Exception e) {
-                throw new IllegalArgumentException("Could not parse " + fullState);
-            }
-        }
-
-        @Override
-        public String toString() {
-            return fullState;
-        }
-    }
-
-    /**
-     * Helper class used to assert the result of a Shell command.
-     */
-    protected static interface ExpectResultChecker {
-        /**
-         * Checkes whether the result of the command matched the expectation.
-         */
-        boolean isExpected(String result);
-        /**
-         * Gets the expected result so it's displayed on log and failure messages.
-         */
-        String getExpected();
-    }
-
-    protected void setRestrictedNetworkingMode(boolean enabled) throws Exception {
-        executeSilentShellCommand(
-                "settings put global restricted_networking_mode " + (enabled ? 1 : 0));
-        assertRestrictedNetworkingModeState(enabled);
-    }
-
-    protected void assertRestrictedNetworkingModeState(boolean enabled) throws Exception {
-        assertDelayedShellCommand("cmd netpolicy get restricted-mode",
-                "Restricted mode status: " + (enabled ? "enabled" : "disabled"));
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java
deleted file mode 100644
index f1858d6..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class AppIdleMeteredTest extends AbstractAppIdleTestCase {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java
deleted file mode 100644
index e737a6d..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AppIdleNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class AppIdleNonMeteredTest extends AbstractAppIdleTestCase {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java
deleted file mode 100644
index c78ca2e..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class BatterySaverModeMeteredTest extends AbstractBatterySaverModeTestCase {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java
deleted file mode 100644
index fb52a54..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/BatterySaverModeNonMeteredTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-
-import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class BatterySaverModeNonMeteredTest extends AbstractBatterySaverModeTestCase {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
deleted file mode 100644
index c1d576d..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.DOZE_MODE;
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class ConnOnActivityStartTest extends AbstractRestrictBackgroundNetworkTestCase {
-    private static final int TEST_ITERATION_COUNT = 5;
-
-    @Before
-    public final void setUp() throws Exception {
-        super.setUp();
-        resetDeviceState();
-    }
-
-    @After
-    public final void tearDown() throws Exception {
-        super.tearDown();
-        resetDeviceState();
-    }
-
-    private void resetDeviceState() throws Exception {
-        resetBatteryState();
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setAppIdle(false);
-        setDozeMode(false);
-    }
-
-
-    @Test
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    public void testStartActivity_batterySaver() throws Exception {
-        setBatterySaverMode(true);
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
-    }
-
-    @Test
-    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-    public void testStartActivity_dataSaver() throws Exception {
-        setRestrictBackground(true);
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
-    }
-
-    @Test
-    @RequiredProperties({DOZE_MODE})
-    public void testStartActivity_doze() throws Exception {
-        setDozeMode(true);
-        // TODO (235284115): We need to turn on Doze every time before starting
-        // the activity.
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_doze", null);
-    }
-
-    @Test
-    @RequiredProperties({APP_STANDBY_MODE})
-    public void testStartActivity_appStandby() throws Exception {
-        turnBatteryOn();
-        setAppIdle(true);
-        // TODO (235284115): We need to put the app into app standby mode every
-        // time before starting the activity.
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby", null);
-    }
-
-    @Test
-    public void testStartActivity_default() throws Exception {
-        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-        assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-        });
-    }
-
-    private void assertLaunchedActivityHasNetworkAccess(String testName,
-            ThrowingRunnable onBeginIteration) throws Exception {
-        for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
-            if (onBeginIteration != null) {
-                onBeginIteration.run();
-            }
-            Log.i(TAG, testName + " start #" + i);
-            launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-            getUiDevice().pressHome();
-            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-            Log.i(TAG, testName + " end #" + i);
-        }
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
deleted file mode 100644
index 790e031..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataSaverModeTest.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
-
-import static com.android.compatibility.common.util.FeatureUtil.isTV;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-import static com.android.cts.net.hostside.Property.NO_DATA_SAVER_MODE;
-
-import static org.junit.Assert.fail;
-
-import androidx.test.filters.LargeTest;
-
-import com.android.compatibility.common.util.CddTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
-@LargeTest
-public class DataSaverModeTest extends AbstractRestrictBackgroundNetworkTestCase {
-
-    private static final String[] REQUIRED_WHITELISTED_PACKAGES = {
-        "com.android.providers.downloads"
-    };
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        setRestrictBackground(false);
-        removeRestrictBackgroundWhitelist(mUid);
-        removeRestrictBackgroundBlacklist(mUid);
-
-        registerBroadcastReceiver();
-        assertRestrictBackgroundChangedReceived(0);
-   }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        setRestrictBackground(false);
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_disabled() throws Exception {
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-
-        // Verify status is always disabled, never whitelisted
-        addRestrictBackgroundWhitelist(mUid);
-        assertRestrictBackgroundChangedReceived(0);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_whitelisted() throws Exception {
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(1);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        addRestrictBackgroundWhitelist(mUid);
-        assertRestrictBackgroundChangedReceived(2);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
-
-        removeRestrictBackgroundWhitelist(mUid);
-        assertRestrictBackgroundChangedReceived(3);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_enabled() throws Exception {
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(1);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        // Make sure foreground app doesn't lose access upon enabling Data Saver.
-        setRestrictBackground(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        setRestrictBackground(true);
-        assertTopNetworkAccess(true);
-
-        // Although it should not have access while the screen is off.
-        turnScreenOff();
-        assertBackgroundNetworkAccess(false);
-        turnScreenOn();
-        // On some TVs, it is possible that the activity on top may change after the screen is
-        // turned off and on again, so relaunch the activity in the test app again.
-        if (isTV()) {
-            startActivity();
-        }
-        assertTopNetworkAccess(true);
-
-        // Goes back to background state.
-        finishActivity();
-        assertBackgroundNetworkAccess(false);
-
-        // Make sure foreground service doesn't lose access upon enabling Data Saver.
-        setRestrictBackground(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
-        setRestrictBackground(true);
-        assertForegroundServiceNetworkAccess();
-        stopForegroundService();
-        assertBackgroundNetworkAccess(false);
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_blacklisted() throws Exception {
-        addRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(1);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        assertsForegroundAlwaysHasNetworkAccess();
-        assertRestrictBackgroundChangedReceived(1);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-
-        // UID policies live by the Highlander rule: "There can be only one".
-        // Hence, if app is whitelisted, it should not be blacklisted anymore.
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(2);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-        addRestrictBackgroundWhitelist(mUid);
-        assertRestrictBackgroundChangedReceived(3);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_WHITELISTED);
-
-        // Check status after removing blacklist.
-        // ...re-enables first
-        addRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(4);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-        assertsForegroundAlwaysHasNetworkAccess();
-        // ... remove blacklist - access's still rejected because Data Saver is on
-        removeRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(4);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_ENABLED);
-        assertsForegroundAlwaysHasNetworkAccess();
-        // ... finally, disable Data Saver
-        setRestrictBackground(false);
-        assertRestrictBackgroundChangedReceived(5);
-        assertDataSaverStatusOnBackground(RESTRICT_BACKGROUND_STATUS_DISABLED);
-        assertsForegroundAlwaysHasNetworkAccess();
-    }
-
-    @Test
-    public void testGetRestrictBackgroundStatus_requiredWhitelistedPackages() throws Exception {
-        final StringBuilder error = new StringBuilder();
-        for (String packageName : REQUIRED_WHITELISTED_PACKAGES) {
-            int uid = -1;
-            try {
-                uid = getUid(packageName);
-                assertRestrictBackgroundWhitelist(uid, true);
-            } catch (Throwable t) {
-                error.append("\nFailed for '").append(packageName).append("'");
-                if (uid > 0) {
-                    error.append(" (uid ").append(uid).append(")");
-                }
-                error.append(": ").append(t).append("\n");
-            }
-        }
-        if (error.length() > 0) {
-            fail(error.toString());
-        }
-    }
-
-    @RequiredProperties({NO_DATA_SAVER_MODE})
-    @CddTest(requirement="7.4.7/C-2-2")
-    @Test
-    public void testBroadcastNotSentOnUnsupportedDevices() throws Exception {
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(0);
-
-        setRestrictBackground(false);
-        assertRestrictBackgroundChangedReceived(0);
-
-        setRestrictBackground(true);
-        assertRestrictBackgroundChangedReceived(0);
-    }
-
-    private void assertDataSaverStatusOnBackground(int expectedStatus) throws Exception {
-        assertRestrictBackgroundStatus(expectedStatus);
-        assertBackgroundNetworkAccess(expectedStatus != RESTRICT_BACKGROUND_STATUS_ENABLED);
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataWarningReceiverTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataWarningReceiverTest.java
deleted file mode 100644
index 13bbab6..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DataWarningReceiverTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.clearSnoozeTimestamps;
-
-import android.content.pm.PackageManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.SubscriptionPlan;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.Direction;
-import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
-
-import com.android.compatibility.common.util.SystemUtil;
-import com.android.compatibility.common.util.UiAutomatorUtils2;
-
-import org.junit.After;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.time.Period;
-import java.time.ZonedDateTime;
-import java.util.Arrays;
-import java.util.List;
-
-public class DataWarningReceiverTest extends AbstractRestrictBackgroundNetworkTestCase {
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        clearSnoozeTimestamps();
-        registerBroadcastReceiver();
-        turnScreenOn();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-    }
-
-    @Test
-    public void testSnoozeWarningNotReceived() throws Exception {
-        Assume.assumeTrue("Feature not supported: " + PackageManager.FEATURE_TELEPHONY,
-                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY));
-        final SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
-        final int subId = SubscriptionManager.getDefaultDataSubscriptionId();
-        Assume.assumeTrue("Valid subId not found",
-                subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID);
-
-        setSubPlanOwner(subId, TEST_PKG);
-        final List<SubscriptionPlan> originalPlans = sm.getSubscriptionPlans(subId);
-        try {
-            // In NetworkPolicyManagerService class, we set the data warning bytes to 90% of
-            // data limit bytes. So, create the subscription plan in such a way this data warning
-            // threshold is already reached.
-            final SubscriptionPlan plan = SubscriptionPlan.Builder
-                    .createRecurring(ZonedDateTime.parse("2007-03-14T00:00:00.000Z"),
-                            Period.ofMonths(1))
-                    .setTitle("CTS")
-                    .setDataLimit(1_000_000_000, SubscriptionPlan.LIMIT_BEHAVIOR_THROTTLED)
-                    .setDataUsage(999_000_000, System.currentTimeMillis())
-                    .build();
-            sm.setSubscriptionPlans(subId, Arrays.asList(plan));
-            final UiDevice uiDevice = UiDevice.getInstance(mInstrumentation);
-            uiDevice.openNotification();
-            try {
-                final UiObject2 uiObject = UiAutomatorUtils2.waitFindObject(
-                        By.text("Data warning"));
-                Assume.assumeNotNull(uiObject);
-                uiObject.wait(Until.clickable(true), 10_000L);
-                uiObject.getParent().swipe(Direction.RIGHT, 1.0f);
-            } catch (Throwable t) {
-                Assume.assumeNoException(
-                        "Error occurred while finding and swiping the notification", t);
-            }
-            assertSnoozeWarningNotReceived();
-            uiDevice.pressHome();
-        } finally {
-            sm.setSubscriptionPlans(subId, originalPlans);
-            setSubPlanOwner(subId, "");
-        }
-    }
-
-    private static void setSubPlanOwner(int subId, String packageName) throws Exception {
-        SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
-                "cmd netpolicy set sub-plan-owner " + subId + " " + packageName);
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java
deleted file mode 100644
index f3a1026..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class DefaultRestrictionsMeteredTest extends AbstractDefaultRestrictionsTest {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java
deleted file mode 100644
index 5651dd0..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class DefaultRestrictionsNonMeteredTest extends AbstractDefaultRestrictionsTest {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java
deleted file mode 100644
index 4306c99..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class DozeModeMeteredTest extends AbstractDozeModeTestCase {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java
deleted file mode 100644
index 1e89f15..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DozeModeNonMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
-
-@RequiredProperties({NON_METERED_NETWORK})
-public class DozeModeNonMeteredTest extends AbstractDozeModeTestCase {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
deleted file mode 100644
index 07434b1..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_APP2_PKG;
-import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TEST_PKG;
-
-import android.os.Environment;
-import android.os.FileUtils;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.OnFailureRule;
-
-import org.junit.AssumptionViolatedException;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-
-public class DumpOnFailureRule extends OnFailureRule {
-    private File mDumpDir = new File(Environment.getExternalStorageDirectory(),
-            "CtsHostsideNetworkTests");
-
-    @Override
-    public void onTestFailure(Statement base, Description description, Throwable throwable) {
-        if (throwable instanceof AssumptionViolatedException) {
-            final String testName = description.getClassName() + "_" + description.getMethodName();
-            Log.d(TAG, "Skipping test " + testName + ": " + throwable);
-            return;
-        }
-
-        prepareDumpRootDir();
-        final String shortenedTestName = getShortenedTestName(description);
-        final File dumpFile = new File(mDumpDir, "dump-" + shortenedTestName);
-        Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath());
-        try (FileOutputStream out = new FileOutputStream(dumpFile)) {
-            for (String cmd : new String[] {
-                    "dumpsys netpolicy",
-                    "dumpsys network_management",
-                    "dumpsys usagestats " + TEST_PKG + " " + TEST_APP2_PKG,
-                    "dumpsys usagestats appstandby",
-                    "dumpsys connectivity trafficcontroller",
-                    "dumpsys netd trafficcontroller",
-                    "dumpsys platform_compat", // TODO (b/279829773): Remove this dump
-                    "dumpsys jobscheduler " + TEST_APP2_PKG, // TODO (b/288220398): Remove this dump
-            }) {
-                dumpCommandOutput(out, cmd);
-            }
-        } catch (FileNotFoundException e) {
-            Log.e(TAG, "Error opening file: " + dumpFile, e);
-        } catch (IOException e) {
-            Log.e(TAG, "Error closing file: " + dumpFile, e);
-        }
-        final UiDevice uiDevice = UiDevice.getInstance(
-                InstrumentationRegistry.getInstrumentation());
-        final File screenshotFile = new File(mDumpDir, "sc-" + shortenedTestName + ".png");
-        uiDevice.takeScreenshot(screenshotFile);
-        final File windowHierarchyFile = new File(mDumpDir, "wh-" + shortenedTestName + ".xml");
-        try {
-            uiDevice.dumpWindowHierarchy(windowHierarchyFile);
-        } catch (IOException e) {
-            Log.e(TAG, "Error dumping window hierarchy", e);
-        }
-    }
-
-    private String getShortenedTestName(Description description) {
-        final String qualifiedClassName = description.getClassName();
-        final String className = qualifiedClassName.substring(
-                qualifiedClassName.lastIndexOf(".") + 1);
-        final String shortenedClassName = className.chars()
-                .filter(Character::isUpperCase)
-                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
-                .toString();
-        return shortenedClassName + "_" + description.getMethodName();
-    }
-
-    void dumpCommandOutput(FileOutputStream out, String cmd) {
-        final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation()
-                .getUiAutomation().executeShellCommand(cmd);
-        try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
-            out.write(("Output of '" + cmd + "':\n").getBytes(StandardCharsets.UTF_8));
-            FileUtils.copy(in, out);
-            out.write("\n\n=================================================================\n\n"
-                    .getBytes(StandardCharsets.UTF_8));
-        } catch (IOException e) {
-            Log.e(TAG, "Error dumping '" + cmd + "'", e);
-        }
-    }
-
-    void prepareDumpRootDir() {
-        if (!mDumpDir.exists() && !mDumpDir.mkdir()) {
-            Log.e(TAG, "Error creating " + mDumpDir);
-        }
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java
deleted file mode 100644
index 3809534..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-
-@RequiredProperties({METERED_NETWORK})
-public class ExpeditedJobMeteredTest extends AbstractExpeditedJobTest {
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
deleted file mode 100644
index 5c99c67..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setupActiveNetworkMeteredness;
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
-
-import android.util.ArraySet;
-
-import com.android.compatibility.common.util.BeforeAfterRule;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-public class MeterednessConfigurationRule extends BeforeAfterRule {
-    private ThrowingRunnable mMeterednessResetter;
-
-    @Override
-    public void onBefore(Statement base, Description description) throws Throwable {
-        final ArraySet<Property> requiredProperties
-                = RequiredPropertiesRule.getRequiredProperties();
-        if (requiredProperties.contains(METERED_NETWORK)) {
-            configureNetworkMeteredness(true);
-        } else if (requiredProperties.contains(NON_METERED_NETWORK)) {
-            configureNetworkMeteredness(false);
-        }
-    }
-
-    @Override
-    public void onAfter(Statement base, Description description) throws Throwable {
-        resetNetworkMeteredness();
-    }
-
-    public void configureNetworkMeteredness(boolean metered) throws Exception {
-        mMeterednessResetter = setupActiveNetworkMeteredness(metered);
-    }
-
-    public void resetNetworkMeteredness() throws Exception {
-        if (mMeterednessResetter != null) {
-            mMeterednessResetter.run();
-            mMeterednessResetter = null;
-        }
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java
deleted file mode 100644
index c9edda6..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MixedModesTest.java
+++ /dev/null
@@ -1,370 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
-import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.DOZE_MODE;
-import static com.android.cts.net.hostside.Property.METERED_NETWORK;
-import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Test cases for the more complex scenarios where multiple restrictions (like Battery Saver Mode
- * and Data Saver Mode) are applied simultaneously.
- * <p>
- * <strong>NOTE: </strong>it might sound like the test methods on this class are testing too much,
- * which would make it harder to diagnose individual failures, but the assumption is that such
- * failure most likely will happen when the restriction is tested individually as well.
- */
-public class MixedModesTest extends AbstractRestrictBackgroundNetworkTestCase {
-    private static final String TAG = "MixedModesTest";
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        // Set initial state.
-        removeRestrictBackgroundWhitelist(mUid);
-        removeRestrictBackgroundBlacklist(mUid);
-        removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-
-        registerBroadcastReceiver();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        try {
-            setRestrictBackground(false);
-        } finally {
-            setBatterySaverMode(false);
-        }
-    }
-
-    /**
-     * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on metered networks.
-     */
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK})
-    @Test
-    public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
-        final MeterednessConfigurationRule meterednessConfiguration
-                = new MeterednessConfigurationRule();
-        meterednessConfiguration.configureNetworkMeteredness(true);
-        try {
-            setRestrictBackground(true);
-            setBatterySaverMode(true);
-
-            Log.v(TAG, "Not whitelisted for any.");
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-
-            Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
-            addRestrictBackgroundWhitelist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundWhitelist(mUid);
-
-            Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            removeRestrictBackgroundWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-
-            Log.v(TAG, "Whitelisted for both.");
-            addRestrictBackgroundWhitelist(mUid);
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(true);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundWhitelist(mUid);
-
-            Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
-            addRestrictBackgroundBlacklist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundBlacklist(mUid);
-
-            Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
-            addRestrictBackgroundBlacklist(mUid);
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundBlacklist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        } finally {
-            meterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    /**
-     * Tests all DS ON and BS ON scenarios from network-policy-restrictions.md on non-metered
-     * networks.
-     */
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, NON_METERED_NETWORK})
-    @Test
-    public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
-        final MeterednessConfigurationRule meterednessConfiguration
-                = new MeterednessConfigurationRule();
-        meterednessConfiguration.configureNetworkMeteredness(false);
-        try {
-            setRestrictBackground(true);
-            setBatterySaverMode(true);
-
-            Log.v(TAG, "Not whitelisted for any.");
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-
-            Log.v(TAG, "Whitelisted for Data Saver but not for Battery Saver.");
-            addRestrictBackgroundWhitelist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundWhitelist(mUid);
-
-            Log.v(TAG, "Whitelisted for Battery Saver but not for Data Saver.");
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            removeRestrictBackgroundWhitelist(mUid);
-            assertBackgroundNetworkAccess(true);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(true);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-
-            Log.v(TAG, "Whitelisted for both.");
-            addRestrictBackgroundWhitelist(mUid);
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(true);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundWhitelist(mUid);
-
-            Log.v(TAG, "Blacklisted for Data Saver, not whitelisted for Battery Saver.");
-            addRestrictBackgroundBlacklist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(false);
-            removeRestrictBackgroundBlacklist(mUid);
-
-            Log.v(TAG, "Blacklisted for Data Saver, whitelisted for Battery Saver.");
-            addRestrictBackgroundBlacklist(mUid);
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-            assertsForegroundAlwaysHasNetworkAccess();
-            assertBackgroundNetworkAccess(true);
-            removeRestrictBackgroundBlacklist(mUid);
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        } finally {
-            meterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    /**
-     * Tests that powersave whitelists works as expected when doze and battery saver modes
-     * are enabled.
-     */
-    @RequiredProperties({DOZE_MODE, BATTERY_SAVER_MODE})
-    @Test
-    public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
-        setBatterySaverMode(true);
-        setDozeMode(true);
-
-        try {
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-
-            addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-
-            removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setBatterySaverMode(false);
-            setDozeMode(false);
-        }
-    }
-
-    /**
-     * Tests that powersave whitelists works as expected when doze and appIdle modes
-     * are enabled.
-     */
-    @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
-    @Test
-    public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
-        setDozeMode(true);
-        setAppIdle(true);
-
-        try {
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-
-            addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-
-            removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setDozeMode(false);
-        }
-    }
-
-    @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
-    @Test
-    public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
-        setDozeMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(true);
-
-            // Wait until the whitelist duration is expired.
-            SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setDozeMode(false);
-        }
-    }
-
-    @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
-    @Test
-    public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
-        setBatterySaverMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(true);
-
-            // Wait until the whitelist duration is expired.
-            SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setBatterySaverMode(false);
-        }
-    }
-
-    /**
-     * Tests that the app idle whitelist works as expected when doze and appIdle mode are enabled.
-     */
-    @RequiredProperties({DOZE_MODE, APP_STANDBY_MODE})
-    @Test
-    public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
-        setDozeMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            // UID still shouldn't have access because of Doze.
-            addAppIdleWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-
-            removeAppIdleWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setDozeMode(false);
-        }
-    }
-
-    @RequiredProperties({APP_STANDBY_MODE, DOZE_MODE})
-    @Test
-    public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        setDozeMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            addAppIdleWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-
-            addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(true);
-
-            // Wait until the whitelist duration is expired.
-            SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setDozeMode(false);
-            removeAppIdleWhitelist(mUid);
-        }
-    }
-
-    @RequiredProperties({APP_STANDBY_MODE, BATTERY_SAVER_MODE})
-    @Test
-    public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        setBatterySaverMode(true);
-        setAppIdle(true);
-
-        try {
-            assertBackgroundNetworkAccess(false);
-
-            addAppIdleWhitelist(mUid);
-            assertBackgroundNetworkAccess(false);
-
-            addTempPowerSaveModeWhitelist(TEST_APP2_PKG, TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(true);
-
-            // Wait until the whitelist duration is expired.
-            SystemClock.sleep(TEMP_POWERSAVE_WHITELIST_DURATION_MS);
-            assertBackgroundNetworkAccess(false);
-        } finally {
-            setAppIdle(false);
-            setBatterySaverMode(false);
-            removeAppIdleWhitelist(mUid);
-        }
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java
deleted file mode 100644
index 0132536..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyNotificationListenerService.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.PendingIntent.CanceledException;
-import android.app.RemoteInput;
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.service.notification.NotificationListenerService;
-import android.service.notification.StatusBarNotification;
-import android.util.Log;
-
-/**
- * NotificationListenerService implementation that executes the notification actions once they're
- * created.
- */
-public class MyNotificationListenerService extends NotificationListenerService {
-    private static final String TAG = "MyNotificationListenerService";
-
-    @Override
-    public void onListenerConnected() {
-        Log.d(TAG, "onListenerConnected()");
-    }
-
-    @Override
-    public void onNotificationPosted(StatusBarNotification sbn) {
-        Log.d(TAG, "onNotificationPosted(): "  + sbn);
-        if (!sbn.getPackageName().startsWith(getPackageName())) {
-            Log.v(TAG, "ignoring notification from a different package");
-            return;
-        }
-        final PendingIntentSender sender = new PendingIntentSender();
-        final Notification notification = sbn.getNotification();
-        if (notification.contentIntent != null) {
-            sender.send("content", notification.contentIntent);
-        }
-        if (notification.deleteIntent != null) {
-            sender.send("delete", notification.deleteIntent);
-        }
-        if (notification.fullScreenIntent != null) {
-            sender.send("full screen", notification.fullScreenIntent);
-        }
-        if (notification.actions != null) {
-            for (Notification.Action action : notification.actions) {
-                sender.send("action", action.actionIntent);
-                sender.send("action extras", action.getExtras());
-                final RemoteInput[] remoteInputs = action.getRemoteInputs();
-                if (remoteInputs != null && remoteInputs.length > 0) {
-                    for (RemoteInput remoteInput : remoteInputs) {
-                        sender.send("remote input extras", remoteInput.getExtras());
-                    }
-                }
-            }
-        }
-        sender.send("notification extras", notification.extras);
-    }
-
-    static String getId() {
-        return String.format("%s/%s", MyNotificationListenerService.class.getPackage().getName(),
-                MyNotificationListenerService.class.getName());
-    }
-
-    static ComponentName getComponentName() {
-        return new ComponentName(MyNotificationListenerService.class.getPackage().getName(),
-                MyNotificationListenerService.class.getName());
-    }
-
-    private static final class PendingIntentSender {
-        private PendingIntent mSentIntent = null;
-        private String mReason = null;
-
-        private void send(String reason, PendingIntent pendingIntent) {
-            if (pendingIntent == null) {
-                // Could happen on action that only has extras
-                Log.v(TAG, "Not sending null pending intent for " + reason);
-                return;
-            }
-            if (mSentIntent != null || mReason != null) {
-                // Sanity check: make sure test case set up just one pending intent in the
-                // notification, otherwise it could pass because another pending intent caused the
-                // whitelisting.
-                throw new IllegalStateException("Already sent a PendingIntent (" + mSentIntent
-                        + ") for reason '" + mReason + "' when requested another for '" + reason
-                        + "' (" + pendingIntent + ")");
-            }
-            Log.i(TAG, "Sending pending intent for " + reason + ":" + pendingIntent);
-            try {
-                pendingIntent.send();
-                mSentIntent = pendingIntent;
-                mReason = reason;
-            } catch (CanceledException e) {
-                Log.w(TAG, "Pending intent " + pendingIntent + " canceled");
-            }
-        }
-
-        private void send(String reason, Bundle extras) {
-            if (extras != null) {
-                for (String key : extras.keySet()) {
-                    Object value = extras.get(key);
-                    if (value instanceof PendingIntent) {
-                        send(reason + " with key '" + key + "'", (PendingIntent) value);
-                    }
-                }
-            }
-        }
-
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
deleted file mode 100644
index 980ecd5..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside;
-
-import android.app.job.JobInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.net.NetworkRequest;
-import android.os.ConditionVariable;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-public class MyServiceClient {
-    private static final int TIMEOUT_MS = 20_000;
-    private static final String PACKAGE = MyServiceClient.class.getPackage().getName();
-    private static final String APP2_PACKAGE = PACKAGE + ".app2";
-    private static final String SERVICE_NAME = APP2_PACKAGE + ".MyService";
-
-    private Context mContext;
-    private ServiceConnection mServiceConnection;
-    private volatile IMyService mService;
-    private final ConditionVariable mServiceCondition = new ConditionVariable();
-
-    public MyServiceClient(Context context) {
-        mContext = context;
-    }
-
-    /**
-     * Binds to a service in the test app to communicate state.
-     * @param bindPriorityFlags Flags to influence the process-state of the bound app.
-     */
-    public void bind(int bindPriorityFlags) {
-        if (mService != null) {
-            throw new IllegalStateException("Already bound");
-        }
-        mServiceConnection = new ServiceConnection() {
-            @Override
-            public void onServiceConnected(ComponentName name, IBinder service) {
-                mService = IMyService.Stub.asInterface(service);
-                mServiceCondition.open();
-            }
-            @Override
-            public void onServiceDisconnected(ComponentName name) {
-                mServiceCondition.close();
-                mService = null;
-            }
-        };
-
-        final Intent intent = new Intent();
-        intent.setComponent(new ComponentName(APP2_PACKAGE, SERVICE_NAME));
-        // Needs to use BIND_NOT_FOREGROUND so app2 does not run in
-        // the same process state as app
-        mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE
-                | bindPriorityFlags);
-        ensureServiceConnection();
-    }
-
-    public void unbind() {
-        if (mService != null) {
-            mContext.unbindService(mServiceConnection);
-        }
-    }
-
-    private void ensureServiceConnection() {
-        if (mService != null) {
-            return;
-        }
-        mServiceCondition.block(TIMEOUT_MS);
-        if (mService == null) {
-            throw new IllegalStateException(
-                    "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
-        }
-    }
-
-    public void registerBroadcastReceiver() throws RemoteException {
-        ensureServiceConnection();
-        mService.registerBroadcastReceiver();
-    }
-
-    public int getCounters(String receiverName, String action) throws RemoteException {
-        ensureServiceConnection();
-        return mService.getCounters(receiverName, action);
-    }
-
-    public String checkNetworkStatus() throws RemoteException {
-        ensureServiceConnection();
-        return mService.checkNetworkStatus();
-    }
-
-    public String getRestrictBackgroundStatus() throws RemoteException {
-        ensureServiceConnection();
-        return mService.getRestrictBackgroundStatus();
-    }
-
-    public void sendNotification(int notificationId, String notificationType)
-            throws RemoteException {
-        ensureServiceConnection();
-        mService.sendNotification(notificationId, notificationType);
-    }
-
-    public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
-            throws RemoteException {
-        ensureServiceConnection();
-        mService.registerNetworkCallback(request, cb);
-    }
-
-    public void unregisterNetworkCallback() throws RemoteException {
-        ensureServiceConnection();
-        mService.unregisterNetworkCallback();
-    }
-
-    public int scheduleJob(JobInfo jobInfo) throws RemoteException {
-        ensureServiceConnection();
-        return mService.scheduleJob(jobInfo);
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
deleted file mode 100644
index 5552b8f..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ /dev/null
@@ -1,427 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
-import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
-
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
-
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.net.cts.util.CtsNetUtils;
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.modules.utils.build.SdkLevel;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.Objects;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
-    private Network mNetwork;
-    private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
-    private CtsNetUtils mCtsNetUtils;
-    private static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
-
-    @Rule
-    public final MeterednessConfigurationRule mMeterednessConfiguration
-            = new MeterednessConfigurationRule();
-
-    enum CallbackState {
-        NONE,
-        AVAILABLE,
-        LOST,
-        BLOCKED_STATUS,
-        CAPABILITIES
-    }
-
-    private static class CallbackInfo {
-        public final CallbackState state;
-        public final Network network;
-        public final Object arg;
-
-        CallbackInfo(CallbackState s, Network n, Object o) {
-            state = s; network = n; arg = o;
-        }
-
-        public String toString() {
-            return String.format("%s (%s) (%s)", state, network, arg);
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (!(o instanceof CallbackInfo)) return false;
-            // Ignore timeMs, since it's unpredictable.
-            final CallbackInfo other = (CallbackInfo) o;
-            return (state == other.state) && Objects.equals(network, other.network)
-                    && Objects.equals(arg, other.arg);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(state, network, arg);
-        }
-    }
-
-    private class TestNetworkCallback extends INetworkCallback.Stub {
-        private static final int TEST_CONNECT_TIMEOUT_MS = 30_000;
-        private static final int TEST_CALLBACK_TIMEOUT_MS = 5_000;
-
-        private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
-
-        protected void setLastCallback(CallbackState state, Network network, Object o) {
-            mCallbacks.offer(new CallbackInfo(state, network, o));
-        }
-
-        CallbackInfo nextCallback(int timeoutMs) {
-            CallbackInfo cb = null;
-            try {
-                cb = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS);
-            } catch (InterruptedException e) {
-            }
-            if (cb == null) {
-                fail("Did not receive callback after " + timeoutMs + "ms");
-            }
-            return cb;
-        }
-
-        CallbackInfo expectCallback(CallbackState state, Network expectedNetwork, Object o) {
-            final CallbackInfo expected = new CallbackInfo(state, expectedNetwork, o);
-            final CallbackInfo actual = nextCallback(TEST_CALLBACK_TIMEOUT_MS);
-            assertEquals("Unexpected callback:", expected, actual);
-            return actual;
-        }
-
-        @Override
-        public void onAvailable(Network network) {
-            setLastCallback(CallbackState.AVAILABLE, network, null);
-        }
-
-        @Override
-        public void onLost(Network network) {
-            setLastCallback(CallbackState.LOST, network, null);
-        }
-
-        @Override
-        public void onBlockedStatusChanged(Network network, boolean blocked) {
-            setLastCallback(CallbackState.BLOCKED_STATUS, network, blocked);
-        }
-
-        @Override
-        public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
-            setLastCallback(CallbackState.CAPABILITIES, network, cap);
-        }
-
-        public Network expectAvailableCallbackAndGetNetwork() {
-            final CallbackInfo cb = nextCallback(TEST_CONNECT_TIMEOUT_MS);
-            if (cb.state != CallbackState.AVAILABLE) {
-                fail("Network is not available. Instead obtained the following callback :" + cb);
-            }
-            return cb.network;
-        }
-
-        public void drainAndWaitForIdle() {
-            try {
-                do {
-                    mCallbacks.drainTo(new ArrayList<>());
-                } while (mCallbacks.poll(TEST_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS) != null);
-            } catch (InterruptedException ie) {
-                Log.e(TAG, "Interrupted while draining callback queue", ie);
-                Thread.currentThread().interrupt();
-            }
-        }
-
-        public void expectBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) {
-            expectCallback(CallbackState.BLOCKED_STATUS, expectedNetwork, expectBlocked);
-        }
-
-        public void expectBlockedStatusCallbackEventually(Network expectedNetwork,
-                boolean expectBlocked) {
-            final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
-            do {
-                final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
-                if (cb.state == CallbackState.BLOCKED_STATUS
-                        && cb.network.equals(expectedNetwork)) {
-                    assertEquals(expectBlocked, cb.arg);
-                    return;
-                }
-            } while (System.currentTimeMillis() <= deadline);
-            fail("Didn't receive onBlockedStatusChanged()");
-        }
-
-        public void expectCapabilitiesCallbackEventually(Network expectedNetwork, boolean hasCap,
-                int cap) {
-            final long deadline = System.currentTimeMillis() + TEST_CALLBACK_TIMEOUT_MS;
-            do {
-                final CallbackInfo cb = nextCallback((int) (deadline - System.currentTimeMillis()));
-                if (cb.state != CallbackState.CAPABILITIES
-                        || !expectedNetwork.equals(cb.network)
-                        || (hasCap != ((NetworkCapabilities) cb.arg).hasCapability(cap))) {
-                    Log.i("NetworkCallbackTest#expectCapabilitiesCallback",
-                            "Ignoring non-matching callback : " + cb);
-                    continue;
-                }
-                // Found a match, return
-                return;
-            } while (System.currentTimeMillis() <= deadline);
-            fail("Didn't receive the expected callback to onCapabilitiesChanged(). Check the "
-                    + "log for a list of received callbacks, if any.");
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        assumeTrue(canChangeActiveNetworkMeteredness());
-
-        registerBroadcastReceiver();
-
-        removeRestrictBackgroundWhitelist(mUid);
-        removeRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(0);
-
-        // Initial state
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setAppIdle(false);
-
-        // Get transports of the active network, this has to be done before changing meteredness,
-        // since wifi will be disconnected when changing from non-metered to metered.
-        final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
-
-        // Mark network as metered.
-        mMeterednessConfiguration.configureNetworkMeteredness(true);
-
-        // Register callback, copy the capabilities from the active network to expect the "original"
-        // network before disconnecting, but null out some fields to prevent over-specified.
-        registerNetworkCallback(new NetworkRequest.Builder()
-                .setCapabilities(networkCapabilities.setTransportInfo(null))
-                .removeCapability(NET_CAPABILITY_NOT_METERED)
-                .setSignalStrength(SIGNAL_STRENGTH_UNSPECIFIED).build(), mTestNetworkCallback);
-        // Wait for onAvailable() callback to ensure network is available before the test
-        // and store the default network.
-        mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork();
-        // Check that the network is metered.
-        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
-                false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        mTestNetworkCallback.drainAndWaitForIdle();
-
-        // Before Android T, DNS queries over private DNS should be but are not restricted by Power
-        // Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
-        // DNS queries when its network is restricted by Power Saver. The fix takes effect backwards
-        // starting from Android T. But for Data Saver, the fix is not backward compatible since
-        // there are some platform changes involved. It is only available on devices that a specific
-        // trunk flag is enabled.
-        //
-        // This test can not only verify that the network traffic from apps is blocked at the right
-        // time, but also verify whether it is correctly blocked at the DNS stage, or at a later
-        // socket connection stage.
-        if (SdkLevel.isAtLeastT()) {
-            // Enable private DNS
-            mCtsNetUtils = new CtsNetUtils(mContext);
-            mCtsNetUtils.storePrivateDnsSetting();
-            mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
-            mCtsNetUtils.awaitPrivateDnsSetting(
-                    "NetworkCallbackTest wait private DNS setting timeout", mNetwork,
-                    GOOGLE_PRIVATE_DNS_SERVER, true);
-        }
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        setRestrictBackground(false);
-        setBatterySaverMode(false);
-        unregisterNetworkCallback();
-
-        if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
-            mCtsNetUtils.restorePrivateDnsSetting();
-        }
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE})
-    @Test
-    public void testOnBlockedStatusChanged_dataSaver() throws Exception {
-        try {
-            // Enable restrict background
-            setRestrictBackground(true);
-            // TODO: Verify expectedUnavailableError when aconfig support mainline.
-            // (see go/aconfig-in-mainline-problems)
-            assertBackgroundNetworkAccess(false);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-
-            // Add to whitelist
-            addRestrictBackgroundWhitelist(mUid);
-            assertBackgroundNetworkAccess(true);
-            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
-            // Remove from whitelist
-            removeRestrictBackgroundWhitelist(mUid);
-            // TODO: Verify expectedUnavailableError when aconfig support mainline.
-            assertBackgroundNetworkAccess(false);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-
-        // Set to non-metered network
-        mMeterednessConfiguration.configureNetworkMeteredness(false);
-        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
-                true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        try {
-            assertBackgroundNetworkAccess(true);
-            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-
-            // Disable restrict background, should not trigger callback
-            setRestrictBackground(false);
-            assertBackgroundNetworkAccess(true);
-            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    @Test
-    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
-        try {
-            // Enable Power Saver
-            setBatterySaverMode(true);
-            if (SdkLevel.isAtLeastT()) {
-                assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-                assertNetworkAccess(false, "java.net.UnknownHostException");
-            } else {
-                assertBackgroundNetworkAccess(false);
-            }
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
-            // Disable Power Saver
-            setBatterySaverMode(false);
-            assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-
-        // Set to non-metered network
-        mMeterednessConfiguration.configureNetworkMeteredness(false);
-        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
-                true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        try {
-            // Enable Power Saver
-            setBatterySaverMode(true);
-            if (SdkLevel.isAtLeastT()) {
-                assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-                assertNetworkAccess(false, "java.net.UnknownHostException");
-            } else {
-                assertBackgroundNetworkAccess(false);
-            }
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-
-            // Disable Power Saver
-            setBatterySaverMode(false);
-            assertBackgroundNetworkAccess(true);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    @Test
-    public void testOnBlockedStatusChanged_default() throws Exception {
-        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-
-        try {
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            assertNetworkAccess(false, null);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
-            launchActivity();
-            assertTopState();
-            assertNetworkAccess(true, null);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-            assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
-
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-            assertNetworkAccess(false, null);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-            assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
-
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-
-        // Set to non-metered network
-        mMeterednessConfiguration.configureNetworkMeteredness(false);
-        mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
-                true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
-        try {
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            assertNetworkAccess(false, null);
-            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-
-            launchActivity();
-            assertTopState();
-            assertNetworkAccess(true, null);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
-            assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
-
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-            assertNetworkAccess(false, null);
-            mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
-            assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
-        } finally {
-            mMeterednessConfiguration.resetNetworkMeteredness();
-        }
-    }
-
-    // TODO: 1. test against VPN lockdown.
-    //       2. test against multiple networks.
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
deleted file mode 100644
index 968e270..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
-import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
-import static android.os.Process.SYSTEM_UID;
-
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertNetworkingBlockedStatusForUid;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isUidNetworkingBlocked;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
-import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import android.os.SystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class NetworkPolicyManagerTest extends AbstractRestrictBackgroundNetworkTestCase {
-    private static final boolean METERED = true;
-    private static final boolean NON_METERED = false;
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        registerBroadcastReceiver();
-
-        removeRestrictBackgroundWhitelist(mUid);
-        removeRestrictBackgroundBlacklist(mUid);
-        assertRestrictBackgroundChangedReceived(0);
-
-        // Initial state
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setRestrictedNetworkingMode(false);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-
-        setBatterySaverMode(false);
-        setRestrictBackground(false);
-        setRestrictedNetworkingMode(false);
-        unregisterNetworkCallback();
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the cases of non-metered network and uid not matched by any rule.
-        // If mUid is not blocked by data saver mode or power saver mode, no matter the network is
-        // metered or non-metered, mUid shouldn't be blocked.
-        assertFalse(isUidNetworkingBlocked(mUid, METERED)); // Match NTWK_ALLOWED_DEFAULT
-        assertFalse(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE})
-    @Test
-    public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the case of uid is system uid.
-        // SYSTEM_UID will never be blocked.
-        assertFalse(isUidNetworkingBlocked(SYSTEM_UID, METERED)); // Match NTWK_ALLOWED_SYSTEM
-        assertFalse(isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
-        try {
-            setRestrictBackground(true);
-            setBatterySaverMode(true);
-            setRestrictedNetworkingMode(true);
-            assertNetworkingBlockedStatusForUid(SYSTEM_UID, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_SYSTEM
-            assertFalse(
-                    isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
-        } finally {
-            setRestrictBackground(false);
-            setBatterySaverMode(false);
-            setRestrictedNetworkingMode(false);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-        }
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE})
-    @Test
-    public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the cases of non-metered network, uid is matched by restrict background blacklist,
-        // uid is matched by restrict background whitelist, app is in the foreground with restrict
-        // background enabled and the app is in the background with restrict background enabled.
-        try {
-            // Enable restrict background and mUid will be blocked because it's not in the
-            // foreground.
-            setRestrictBackground(true);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
-
-            // Although restrict background is enabled and mUid is in the background, but mUid will
-            // not be blocked if network is non-metered.
-            assertFalse(
-                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-
-            // Add mUid into the restrict background blacklist.
-            addRestrictBackgroundBlacklist(mUid);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_DENYLIST
-
-            // Although mUid is in the restrict background blacklist, but mUid won't be blocked if
-            // the network is non-metered.
-            assertFalse(
-                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-            removeRestrictBackgroundBlacklist(mUid);
-
-            // Add mUid into the restrict background whitelist.
-            addRestrictBackgroundWhitelist(mUid);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_ALLOWLIST
-            assertFalse(
-                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-            removeRestrictBackgroundWhitelist(mUid);
-
-            // Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
-            launchActivity();
-            assertTopState();
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
-
-            // Back to background.
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
-        } finally {
-            setRestrictBackground(false);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-        }
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the cases of restricted networking mode enabled.
-        try {
-            // All apps should be blocked if restricted networking mode is enabled except for those
-            // apps who have CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
-            // This test won't test if an app who has CONNECTIVITY_USE_RESTRICTED_NETWORKS will not
-            // be blocked because CONNECTIVITY_USE_RESTRICTED_NETWORKS is a signature/privileged
-            // permission that CTS cannot acquire. Also it's not good for this test to use those
-            // privileged apps which have CONNECTIVITY_USE_RESTRICTED_NETWORKS to test because there
-            // is no guarantee that those apps won't remove this permission someday, and if it
-            // happens, then this test will fail.
-            setRestrictedNetworkingMode(true);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_RESTRICTED_MODE
-            assertTrue(isUidNetworkingBlocked(mUid,
-                    NON_METERED)); // Match NTWK_BLOCKED_RESTRICTED_MODE
-        } finally {
-            setRestrictedNetworkingMode(false);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-        }
-    }
-
-    @RequiredProperties({BATTERY_SAVER_MODE})
-    @Test
-    public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
-        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
-        // test the cases of power saver mode enabled, uid in the power saver mode whitelist and
-        // uid in the power saver mode whitelist with non-metered network.
-        try {
-            // mUid should be blocked if power saver mode is enabled.
-            setBatterySaverMode(true);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    true /* expectedResult */); // Match NTWK_BLOCKED_POWER
-            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_BLOCKED_POWER
-
-            // Add TEST_APP2_PKG into power saver mode whitelist, its uid rule is RULE_ALLOW_ALL and
-            // it shouldn't be blocked.
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-            assertFalse(
-                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        } finally {
-            setBatterySaverMode(false);
-            assertNetworkingBlockedStatusForUid(mUid, METERED,
-                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
-        }
-    }
-
-    @RequiredProperties({DATA_SAVER_MODE})
-    @Test
-    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
-        try {
-            // isUidRestrictedOnMeteredNetworks() will only return true when restrict background is
-            // enabled and mUid is not in the restrict background whitelist and TEST_APP2_PKG is not
-            // in the foreground. For other cases, it will return false.
-            setRestrictBackground(true);
-            assertIsUidRestrictedOnMeteredNetworks(mUid, true /* expectedResult */);
-
-            // Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
-            // return false.
-            launchActivity();
-            assertTopState();
-            assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
-            // Back to background.
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
-
-            // Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
-            // will return false.
-            addRestrictBackgroundWhitelist(mUid);
-            assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
-            removeRestrictBackgroundWhitelist(mUid);
-        } finally {
-            // Restrict background is disabled and isUidRestrictedOnMeteredNetworks() will return
-            // false.
-            setRestrictBackground(false);
-            assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
-        }
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
-        assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
-
-        try {
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-            assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
-            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
-
-            launchActivity();
-            assertTopState();
-            assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
-            assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
-
-            finishActivity();
-            assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
-            SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
-            assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
-            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
-
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
-            assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
-        } finally {
-            removePowerSaveModeWhitelist(TEST_APP2_PKG);
-        }
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java
deleted file mode 100644
index f340907..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestRunner.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
-
-import org.junit.rules.RunRules;
-import org.junit.rules.TestRule;
-import org.junit.runners.model.FrameworkMethod;
-import org.junit.runners.model.InitializationError;
-import org.junit.runners.model.Statement;
-
-import java.util.List;
-
-/**
- * Custom runner to allow dumping logs after a test failure before the @After methods get to run.
- */
-public class NetworkPolicyTestRunner extends AndroidJUnit4ClassRunner {
-    private TestRule mDumpOnFailureRule = new DumpOnFailureRule();
-
-    public NetworkPolicyTestRunner(Class<?> klass) throws InitializationError {
-        super(klass);
-    }
-
-    @Override
-    public Statement methodInvoker(FrameworkMethod method, Object test) {
-        return new RunRules(super.methodInvoker(method, test), List.of(mDumpOnFailureRule),
-                describeChild(method));
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
deleted file mode 100644
index 5331601..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ /dev/null
@@ -1,486 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
-import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
-
-import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
-import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.location.LocationManager;
-import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkPolicyManager;
-import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.ActionListener;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionManager;
-import android.telephony.data.ApnSetting;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.uiautomator.UiDevice;
-
-import com.android.compatibility.common.util.AppStandbyUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-import com.android.compatibility.common.util.PollingCheck;
-import com.android.compatibility.common.util.ShellIdentityUtils;
-import com.android.compatibility.common.util.ThrowingRunnable;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-public class NetworkPolicyTestUtils {
-
-    // android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS
-    // TODO: Expose it as a @TestApi instead of copying the constant
-    private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS =
-            "carrier_metered_apn_types_strings";
-
-    private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000;
-
-    private static ConnectivityManager mCm;
-    private static WifiManager mWm;
-    private static CarrierConfigManager mCarrierConfigManager;
-    private static NetworkPolicyManager sNpm;
-
-    private static Boolean mBatterySaverSupported;
-    private static Boolean mDataSaverSupported;
-    private static Boolean mDozeModeSupported;
-    private static Boolean mAppStandbySupported;
-
-    private NetworkPolicyTestUtils() {}
-
-    public static boolean isBatterySaverSupported() {
-        if (mBatterySaverSupported == null) {
-            mBatterySaverSupported = BatteryUtils.isBatterySaverSupported();
-        }
-        return mBatterySaverSupported;
-    }
-
-    private static boolean isWear() {
-        return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
-    }
-
-    /**
-     * As per CDD requirements, if the device doesn't support data saver mode then
-     * ConnectivityManager.getRestrictBackgroundStatus() will always return
-     * RESTRICT_BACKGROUND_STATUS_DISABLED. So, enable the data saver mode and check if
-     * ConnectivityManager.getRestrictBackgroundStatus() for an app in background returns
-     * RESTRICT_BACKGROUND_STATUS_DISABLED or not.
-     */
-    public static boolean isDataSaverSupported() {
-        if (isWear()) {
-            return false;
-        }
-        if (mDataSaverSupported == null) {
-            setRestrictBackgroundInternal(false);
-            assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
-            try {
-                setRestrictBackgroundInternal(true);
-                mDataSaverSupported = !isMyRestrictBackgroundStatus(
-                        RESTRICT_BACKGROUND_STATUS_DISABLED);
-            } finally {
-                setRestrictBackgroundInternal(false);
-            }
-        }
-        return mDataSaverSupported;
-    }
-
-    public static boolean isDozeModeSupported() {
-        if (mDozeModeSupported == null) {
-            final String result = executeShellCommand("cmd deviceidle enabled deep");
-            mDozeModeSupported = result.equals("1");
-        }
-        return mDozeModeSupported;
-    }
-
-    public static boolean isAppStandbySupported() {
-        if (mAppStandbySupported == null) {
-            mAppStandbySupported = AppStandbyUtils.isAppStandbyEnabled();
-        }
-        return mAppStandbySupported;
-    }
-
-    public static boolean isLowRamDevice() {
-        final ActivityManager am = (ActivityManager) getContext().getSystemService(
-                Context.ACTIVITY_SERVICE);
-        return am.isLowRamDevice();
-    }
-
-    /** Forces JobScheduler to run the job if constraints are met. */
-    public static void forceRunJob(String pkg, int jobId) {
-        executeShellCommand("cmd jobscheduler run -f -u " + UserHandle.myUserId()
-                + " " + pkg + " " + jobId);
-    }
-
-    public static boolean isLocationEnabled() {
-        final LocationManager lm = (LocationManager) getContext().getSystemService(
-                Context.LOCATION_SERVICE);
-        return lm.isLocationEnabled();
-    }
-
-    public static void setLocationEnabled(boolean enabled) {
-        final LocationManager lm = (LocationManager) getContext().getSystemService(
-                Context.LOCATION_SERVICE);
-        lm.setLocationEnabledForUser(enabled, Process.myUserHandle());
-        assertEquals("Couldn't change location enabled state", lm.isLocationEnabled(), enabled);
-        Log.d(TAG, "Changed location enabled state to " + enabled);
-    }
-
-    public static boolean isActiveNetworkMetered(boolean metered) {
-        return getConnectivityManager().isActiveNetworkMetered() == metered;
-    }
-
-    public static boolean canChangeActiveNetworkMeteredness() {
-        final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
-        return networkCapabilities.hasTransport(TRANSPORT_WIFI)
-                || networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
-    }
-
-    /**
-     * Updates the meteredness of the active network. Right now we can only change meteredness
-     * of either Wifi or cellular network, so if the active network is not either of these, this
-     * will throw an exception.
-     *
-     * @return a {@link ThrowingRunnable} object that can used to reset the meteredness change
-     *         made by this method.
-     */
-    public static ThrowingRunnable setupActiveNetworkMeteredness(boolean metered) throws Exception {
-        if (isActiveNetworkMetered(metered)) {
-            return null;
-        }
-        final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
-        if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
-            final String ssid = getWifiSsid();
-            setWifiMeteredStatus(ssid, metered);
-            return () -> setWifiMeteredStatus(ssid, !metered);
-        } else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
-            final int subId = SubscriptionManager.getActiveDataSubscriptionId();
-            setCellularMeteredStatus(subId, metered);
-            return () -> setCellularMeteredStatus(subId, !metered);
-        } else {
-            // Right now, we don't have a way to change meteredness of networks other
-            // than Wi-Fi or Cellular, so just throw an exception.
-            throw new IllegalStateException("Can't change meteredness of current active network");
-        }
-    }
-
-    private static String getWifiSsid() {
-        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-            final String ssid = getWifiManager().getConnectionInfo().getSSID();
-            assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
-            return ssid;
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    static NetworkCapabilities getActiveNetworkCapabilities() {
-        final Network activeNetwork = getConnectivityManager().getActiveNetwork();
-        assertNotNull("No active network available", activeNetwork);
-        return getConnectivityManager().getNetworkCapabilities(activeNetwork);
-    }
-
-    private static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
-        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-            final WifiConfiguration currentConfig = getWifiConfiguration(ssid);
-            currentConfig.meteredOverride = metered
-                    ? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE;
-            BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
-            getWifiManager().save(currentConfig, createActionListener(
-                    blockingQueue, Integer.MAX_VALUE));
-            Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
-                    TimeUnit.MILLISECONDS);
-            if (resultCode == null) {
-                fail("Timed out waiting for meteredness to change; ssid=" + ssid
-                        + ", metered=" + metered);
-            } else if (resultCode != Integer.MAX_VALUE) {
-                fail("Error overriding the meteredness; ssid=" + ssid
-                        + ", metered=" + metered + ", error=" + resultCode);
-            }
-            final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */);
-            if (!success) {
-                Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid);
-                blockingQueue = new LinkedBlockingQueue<>();
-                getWifiManager().connect(currentConfig, createActionListener(
-                        blockingQueue, Integer.MAX_VALUE));
-                resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
-                        TimeUnit.MILLISECONDS);
-                if (resultCode == null) {
-                    fail("Timed out waiting for wifi to connect; ssid=" + ssid);
-                } else if (resultCode != Integer.MAX_VALUE) {
-                    fail("Error connecting to wifi; ssid=" + ssid
-                            + ", error=" + resultCode);
-                }
-                assertActiveNetworkMetered(metered, true /* throwOnFailure */);
-            }
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    private static WifiConfiguration getWifiConfiguration(String ssid) {
-        final List<String> ssids = new ArrayList<>();
-        for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) {
-            if (config.SSID.equals(ssid)) {
-                return config;
-            }
-            ssids.add(config.SSID);
-        }
-        fail("Couldn't find the wifi config; ssid=" + ssid
-                + ", all=" + Arrays.toString(ssids.toArray()));
-        return null;
-    }
-
-    private static ActionListener createActionListener(BlockingQueue<Integer> blockingQueue,
-            int successCode) {
-        return new ActionListener() {
-            @Override
-            public void onSuccess() {
-                blockingQueue.offer(successCode);
-            }
-
-            @Override
-            public void onFailure(int reason) {
-                blockingQueue.offer(reason);
-            }
-        };
-    }
-
-    private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception {
-        final PersistableBundle bundle = new PersistableBundle();
-        bundle.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS,
-                new String[] {ApnSetting.TYPE_MMS_STRING});
-        ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(),
-                (cm) -> cm.overrideConfig(subId, metered ? null : bundle));
-        assertActiveNetworkMetered(metered, true /* throwOnFailure */);
-    }
-
-    private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus,
-            boolean throwOnFailure) throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final NetworkCallback networkCallback = new NetworkCallback() {
-            @Override
-            public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
-                final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
-                if (metered == expectedMeteredStatus) {
-                    latch.countDown();
-                }
-            }
-        };
-        // Registering a callback here guarantees onCapabilitiesChanged is called immediately
-        // with the current setting. Therefore, if the setting has already been changed,
-        // this method will return right away, and if not it will wait for the setting to change.
-        getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
-        try {
-            if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
-                final String errorMsg = "Timed out waiting for active network metered status "
-                        + "to change to " + expectedMeteredStatus + "; network = "
-                        + getConnectivityManager().getActiveNetwork();
-                if (throwOnFailure) {
-                    fail(errorMsg);
-                }
-                Log.w(TAG, errorMsg);
-                return false;
-            }
-            return true;
-        } finally {
-            getConnectivityManager().unregisterNetworkCallback(networkCallback);
-        }
-    }
-
-    public static void setRestrictBackground(boolean enabled) {
-        if (!isDataSaverSupported()) {
-            return;
-        }
-        setRestrictBackgroundInternal(enabled);
-    }
-
-    static void setRestrictBackgroundInternal(boolean enabled) {
-        executeShellCommand("cmd netpolicy set restrict-background " + enabled);
-        final String output = executeShellCommand("cmd netpolicy get restrict-background");
-        final String expectedSuffix = enabled ? "enabled" : "disabled";
-        assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'",
-                output.endsWith(expectedSuffix));
-    }
-
-    public static boolean isMyRestrictBackgroundStatus(int expectedStatus) {
-        final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
-        if (expectedStatus != actualStatus) {
-            Log.d(TAG, "MyRestrictBackgroundStatus: "
-                    + "Expected: " + restrictBackgroundValueToString(expectedStatus)
-                    + "; Actual: " + restrictBackgroundValueToString(actualStatus));
-            return false;
-        }
-        return true;
-    }
-
-    // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
-    private static String unquoteSSID(String ssid) {
-        // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
-        // Otherwise it's guaranteed not to start with a quote.
-        if (ssid.charAt(0) == '"') {
-            return ssid.substring(1, ssid.length() - 1);
-        } else {
-            return ssid;
-        }
-    }
-
-    public static String restrictBackgroundValueToString(int status) {
-        switch (status) {
-            case RESTRICT_BACKGROUND_STATUS_DISABLED:
-                return "DISABLED";
-            case RESTRICT_BACKGROUND_STATUS_WHITELISTED:
-                return "WHITELISTED";
-            case RESTRICT_BACKGROUND_STATUS_ENABLED:
-                return "ENABLED";
-            default:
-                return "UNKNOWN_STATUS_" + status;
-        }
-    }
-
-    public static void clearSnoozeTimestamps() {
-        executeShellCommand("dumpsys netpolicy --unsnooze");
-    }
-
-    public static String executeShellCommand(String command) {
-        final String result = runShellCommandOrThrow(command).trim();
-        Log.d(TAG, "Output of '" + command + "': '" + result + "'");
-        return result;
-    }
-
-    public static void assertMyRestrictBackgroundStatus(int expectedStatus) {
-        final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
-        assertEquals(restrictBackgroundValueToString(expectedStatus),
-                restrictBackgroundValueToString(actualStatus));
-    }
-
-    public static ConnectivityManager getConnectivityManager() {
-        if (mCm == null) {
-            mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        }
-        return mCm;
-    }
-
-    public static WifiManager getWifiManager() {
-        if (mWm == null) {
-            mWm = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
-        }
-        return mWm;
-    }
-
-    public static CarrierConfigManager getCarrierConfigManager() {
-        if (mCarrierConfigManager == null) {
-            mCarrierConfigManager = (CarrierConfigManager) getContext().getSystemService(
-                    Context.CARRIER_CONFIG_SERVICE);
-        }
-        return mCarrierConfigManager;
-    }
-
-    public static NetworkPolicyManager getNetworkPolicyManager() {
-        if (sNpm == null) {
-            sNpm = getContext().getSystemService(NetworkPolicyManager.class);
-        }
-        return sNpm;
-    }
-
-    public static Context getContext() {
-        return getInstrumentation().getContext();
-    }
-
-    public static Instrumentation getInstrumentation() {
-        return InstrumentationRegistry.getInstrumentation();
-    }
-
-    public static UiDevice getUiDevice() {
-        return UiDevice.getInstance(getInstrumentation());
-    }
-
-    // When power saver mode or restrict background enabled or adding any white/black list into
-    // those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
-    // this function and using PollingCheck to try to make sure the uid has updated and reduce the
-    // flaky rate.
-    public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
-            boolean expectedResult) {
-        final String errMsg = String.format("Unexpected result from isUidNetworkingBlocked; "
-                + "uid= " + uid + ", metered=" + metered + ", expected=" + expectedResult);
-        PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)),
-                errMsg);
-    }
-
-    public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult) {
-        final String errMsg = String.format(
-                "Unexpected result from isUidRestrictedOnMeteredNetworks; "
-                + "uid= " + uid + ", expected=" + expectedResult);
-        PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)),
-                errMsg);
-    }
-
-    public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
-        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-            return getNetworkPolicyManager().isUidNetworkingBlocked(uid, meteredNetwork);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    public static boolean isUidRestrictedOnMeteredNetworks(int uid) {
-        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            uiAutomation.adoptShellPermissionIdentity();
-            return getNetworkPolicyManager().isUidRestrictedOnMeteredNetworks(uid);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java
deleted file mode 100644
index 18805f9..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/Property.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDataSaverSupported;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isLowRamDevice;
-
-public enum Property {
-    BATTERY_SAVER_MODE(1 << 0) {
-        public boolean isSupported() { return isBatterySaverSupported(); }
-    },
-
-    DATA_SAVER_MODE(1 << 1) {
-        public boolean isSupported() { return isDataSaverSupported(); }
-    },
-
-    NO_DATA_SAVER_MODE(~DATA_SAVER_MODE.getValue()) {
-        public boolean isSupported() { return !isDataSaverSupported(); }
-    },
-
-    DOZE_MODE(1 << 2) {
-        public boolean isSupported() { return isDozeModeSupported(); }
-    },
-
-    APP_STANDBY_MODE(1 << 3) {
-        public boolean isSupported() { return isAppStandbySupported(); }
-    },
-
-    NOT_LOW_RAM_DEVICE(1 << 4) {
-        public boolean isSupported() { return !isLowRamDevice(); }
-    },
-
-    METERED_NETWORK(1 << 5) {
-        public boolean isSupported() {
-            return isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness();
-        }
-    },
-
-    NON_METERED_NETWORK(~METERED_NETWORK.getValue()) {
-        public boolean isSupported() {
-            return isActiveNetworkMetered(false) || canChangeActiveNetworkMeteredness();
-        }
-    };
-
-    private int mValue;
-
-    Property(int value) { mValue = value; }
-
-    public int getValue() { return mValue; }
-
-    abstract boolean isSupported();
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java
deleted file mode 100644
index 96838bb..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredProperties.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-@Retention(RUNTIME)
-@Target({METHOD, TYPE})
-@Inherited
-public @interface RequiredProperties {
-    Property[] value();
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java
deleted file mode 100644
index 01f9f3e..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RequiredPropertiesRule.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
-
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Log;
-
-import com.android.compatibility.common.util.BeforeAfterRule;
-
-import org.junit.Assume;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-public class RequiredPropertiesRule extends BeforeAfterRule {
-
-    private static ArraySet<Property> mRequiredProperties;
-
-    @Override
-    public void onBefore(Statement base, Description description) {
-        mRequiredProperties = getAllRequiredProperties(description);
-
-        final String testName = description.getClassName() + "#" + description.getMethodName();
-        assertTestIsValid(testName, mRequiredProperties);
-        Log.i(TAG, "Running test " + testName + " with required properties: "
-                + propertiesToString(mRequiredProperties));
-    }
-
-    private ArraySet<Property> getAllRequiredProperties(Description description) {
-        final ArraySet<Property> allRequiredProperties = new ArraySet<>();
-        RequiredProperties requiredProperties = description.getAnnotation(RequiredProperties.class);
-        if (requiredProperties != null) {
-            Collections.addAll(allRequiredProperties, requiredProperties.value());
-        }
-
-        for (Class<?> clazz = description.getTestClass();
-                clazz != null; clazz = clazz.getSuperclass()) {
-            requiredProperties = clazz.getDeclaredAnnotation(RequiredProperties.class);
-            if (requiredProperties == null) {
-                continue;
-            }
-            for (Property requiredProperty : requiredProperties.value()) {
-                for (Property p : Property.values()) {
-                    if (p.getValue() == ~requiredProperty.getValue()
-                            && allRequiredProperties.contains(p)) {
-                        continue;
-                    }
-                }
-                allRequiredProperties.add(requiredProperty);
-            }
-        }
-        return allRequiredProperties;
-    }
-
-    private void assertTestIsValid(String testName, ArraySet<Property> requiredProperies) {
-        if (requiredProperies == null) {
-            return;
-        }
-        final ArrayList<Property> unsupportedProperties = new ArrayList<>();
-        for (Property property : requiredProperies) {
-            if (!property.isSupported()) {
-                unsupportedProperties.add(property);
-            }
-        }
-        Assume.assumeTrue("Unsupported properties: "
-                + propertiesToString(unsupportedProperties), unsupportedProperties.isEmpty());
-    }
-
-    public static ArraySet<Property> getRequiredProperties() {
-        return mRequiredProperties;
-    }
-
-    private static String propertiesToString(Iterable<Property> properties) {
-        return "[" + TextUtils.join(",", properties) + "]";
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
deleted file mode 100644
index 4777bf4..0000000
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.cts.net.hostside;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public final class RestrictedModeTest extends AbstractRestrictBackgroundNetworkTestCase {
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        setRestrictedNetworkingMode(false);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        setRestrictedNetworkingMode(false);
-        super.tearDown();
-    }
-
-    @Test
-    public void testNetworkAccess() throws Exception {
-        // go to foreground state and enable restricted mode
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        setRestrictedNetworkingMode(true);
-        assertTopNetworkAccess(false);
-
-        // go to background state
-        finishActivity();
-        assertBackgroundNetworkAccess(false);
-
-        // disable restricted mode and assert network access in foreground and background states
-        setRestrictedNetworkingMode(false);
-        launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        assertTopNetworkAccess(true);
-
-        // go to background state
-        finishActivity();
-        assertBackgroundNetworkAccess(true);
-    }
-
-    @Test
-    public void testNetworkAccess_withBatterySaver() throws Exception {
-        setBatterySaverMode(true);
-        try {
-            addPowerSaveModeWhitelist(TEST_APP2_PKG);
-            assertBackgroundNetworkAccess(true);
-
-            setRestrictedNetworkingMode(true);
-            // App would be denied network access since Restricted mode is on.
-            assertBackgroundNetworkAccess(false);
-            setRestrictedNetworkingMode(false);
-            // Given that Restricted mode is turned off, app should be able to access network again.
-            assertBackgroundNetworkAccess(true);
-        } finally {
-            setBatterySaverMode(false);
-        }
-    }
-}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 454940f..e186c6b 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -22,7 +22,9 @@
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
 import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.TYPE_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.AF_INET;
@@ -50,6 +52,7 @@
 import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.RecorderCallback.CallbackEntry.BLOCKED_STATUS_INT;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -72,6 +75,7 @@
 import android.database.Cursor;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.InetAddresses;
 import android.net.IpSecManager;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -91,6 +95,7 @@
 import android.net.cts.util.CtsNetUtils;
 import android.net.util.KeepaliveUtils;
 import android.net.wifi.WifiManager;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
@@ -120,6 +125,8 @@
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.PacketBuilder;
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.ConnectUtil;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.RecorderCallback;
@@ -217,6 +224,7 @@
     private WifiManager mWifiManager;
     private RemoteSocketFactoryClient mRemoteSocketFactoryClient;
     private CtsNetUtils mCtsNetUtils;
+    private ConnectUtil mConnectUtil;
     private PackageManager mPackageManager;
     private Context mTestContext;
     private Context mTargetContext;
@@ -230,9 +238,13 @@
     // The registered callbacks.
     private List<NetworkCallback> mRegisteredCallbacks = new ArrayList<>();
 
-    @Rule
+    @Rule(order = 1)
     public final DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
 
+    @Rule(order = 2)
+    public final AutoReleaseNetworkCallbackRule
+            mNetworkCallbackRule = new AutoReleaseNetworkCallbackRule();
+
     private boolean supportedHardware() {
         final PackageManager pm = getInstrumentation().getContext().getPackageManager();
         return !pm.hasSystemFeature("android.hardware.type.watch");
@@ -262,14 +274,15 @@
         mRemoteSocketFactoryClient.bind();
         mDevice.waitForIdle();
         mCtsNetUtils = new CtsNetUtils(mTestContext);
+        mConnectUtil = new ConnectUtil(mTestContext);
         mPackageManager = mTestContext.getPackageManager();
+        assumeTrue(supportedHardware());
     }
 
     @After
     public void tearDown() throws Exception {
         restorePrivateDnsSetting();
         mRemoteSocketFactoryClient.unbind();
-        mCtsNetUtils.tearDown();
         Log.i(TAG, "Stopping VPN");
         stopVpn();
         unregisterRegisteredCallbacks();
@@ -879,17 +892,14 @@
 
     @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
     public void testChangeUnderlyingNetworks() throws Exception {
-        assumeTrue(supportedHardware());
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
         final TestableNetworkCallback callback = new TestableNetworkCallback();
         final boolean isWifiEnabled = mWifiManager.isWifiEnabled();
         testAndCleanup(() -> {
             // Ensure both of wifi and mobile data are connected.
-            final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
-            assertTrue("Wifi is not connected", (wifiNetwork != null));
-            final Network cellNetwork = mCtsNetUtils.connectToCell();
-            assertTrue("Mobile data is not connected", (cellNetwork != null));
+            final Network wifiNetwork = mConnectUtil.ensureWifiValidated();
+            final Network cellNetwork = mNetworkCallbackRule.requestCell();
             // Store current default network.
             final Network defaultNetwork = mCM.getActiveNetwork();
             // Start VPN and set empty array as its underlying networks.
@@ -938,7 +948,6 @@
 
     @Test
     public void testDefault() throws Exception {
-        assumeTrue(supportedHardware());
         if (!SdkLevel.isAtLeastS() && (
                 SystemProperties.getInt("persist.adb.tcp.port", -1) > -1
                         || SystemProperties.getInt("service.adb.tcp.port", -1) > -1)) {
@@ -962,19 +971,30 @@
         final TestableNetworkCallback otherUidCallback = new TestableNetworkCallback();
         final TestableNetworkCallback myUidCallback = new TestableNetworkCallback();
         if (SdkLevel.isAtLeastS()) {
-            final int otherUid =
-                    UserHandle.of(5 /* userId */).getUid(Process.FIRST_APPLICATION_UID);
+            // Using the same appId with the test to make sure otherUid has the internet permission.
+            // This works because the UID permission map only stores the app ID and not the whole
+            // UID. If the otherUid does not have the internet permission, network access from
+            // otherUid could be considered blocked on V+.
+            final int appId = UserHandle.getAppId(Process.myUid());
+            final int otherUid = UserHandle.of(5 /* userId */).getUid(appId);
             final Handler h = new Handler(Looper.getMainLooper());
             runWithShellPermissionIdentity(() -> {
                 registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
                 registerDefaultNetworkCallbackForUid(otherUid, otherUidCallback, h);
                 registerDefaultNetworkCallbackForUid(Process.myUid(), myUidCallback, h);
             }, NETWORK_SETTINGS);
-            for (TestableNetworkCallback callback :
-                    List.of(systemDefaultCallback, otherUidCallback, myUidCallback)) {
+            for (TestableNetworkCallback callback : List.of(systemDefaultCallback, myUidCallback)) {
                 callback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
                         true /* validated */, false /* blocked */, TIMEOUT_MS);
             }
+            // On V+, ConnectivityService generates blockedReasons based on bpf map contents even if
+            // the otherUid does not exist on device. So if the background chain is enabled,
+            // otherUid is blocked.
+            final boolean isOtherUidBlocked = SdkLevel.isAtLeastV()
+                    && runAsShell(NETWORK_SETTINGS, () -> mCM.getFirewallChainEnabled(
+                            FIREWALL_CHAIN_BACKGROUND));
+            otherUidCallback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
+                    true /* validated */, isOtherUidBlocked, TIMEOUT_MS);
         }
 
         FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
@@ -1017,8 +1037,8 @@
             // This needs to be done before testing  private DNS because checkStrictModePrivateDns
             // will set the private DNS server to a nonexistent name, which will cause validation to
             // fail and could cause the default network to switch (e.g., from wifi to cellular).
-            systemDefaultCallback.assertNoCallback();
-            otherUidCallback.assertNoCallback();
+            assertNoCallbackExceptCapOrLpChange(systemDefaultCallback);
+            assertNoCallbackExceptCapOrLpChange(otherUidCallback);
         }
 
         checkStrictModePrivateDns();
@@ -1026,10 +1046,13 @@
         receiver.unregisterQuietly();
     }
 
+    private void assertNoCallbackExceptCapOrLpChange(TestableNetworkCallback callback) {
+        callback.assertNoCallback(c -> !(c instanceof CallbackEntry.CapabilitiesChanged
+                || c instanceof CallbackEntry.LinkPropertiesChanged));
+    }
+
     @Test
     public void testAppAllowed() throws Exception {
-        assumeTrue(supportedHardware());
-
         FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
 
         // Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1137,8 +1160,6 @@
     }
 
     private void doTestAutomaticOnOffKeepaliveMode(final boolean closeSocket) throws Exception {
-        assumeTrue(supportedHardware());
-
         // Get default network first before starting VPN
         final Network defaultNetwork = mCM.getActiveNetwork();
         final TestableNetworkCallback cb = new TestableNetworkCallback();
@@ -1226,8 +1247,6 @@
 
     @Test
     public void testAppDisallowed() throws Exception {
-        assumeTrue(supportedHardware());
-
         FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
         FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
 
@@ -1260,8 +1279,6 @@
 
     @Test
     public void testSocketClosed() throws Exception {
-        assumeTrue(supportedHardware());
-
         final FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
         final List<FileDescriptor> remoteFds = new ArrayList<>();
 
@@ -1285,7 +1302,6 @@
 
     @Test
     public void testExcludedRoutes() throws Exception {
-        assumeTrue(supportedHardware());
         assumeTrue(SdkLevel.isAtLeastT());
 
         // Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1306,8 +1322,6 @@
 
     @Test
     public void testIncludedRoutes() throws Exception {
-        assumeTrue(supportedHardware());
-
         // Shell app must not be put in here or it would kill the ADB-over-network use case
         String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
         startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
@@ -1325,7 +1339,6 @@
 
     @Test
     public void testInterleavedRoutes() throws Exception {
-        assumeTrue(supportedHardware());
         assumeTrue(SdkLevel.isAtLeastT());
 
         // Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1353,8 +1366,6 @@
 
     @Test
     public void testGetConnectionOwnerUidSecurity() throws Exception {
-        assumeTrue(supportedHardware());
-
         DatagramSocket s;
         InetAddress address = InetAddress.getByName("localhost");
         s = new DatagramSocket();
@@ -1375,7 +1386,6 @@
 
     @Test
     public void testSetProxy() throws  Exception {
-        assumeTrue(supportedHardware());
         ProxyInfo initialProxy = mCM.getDefaultProxy();
         // Receiver for the proxy change broadcast.
         BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
@@ -1415,7 +1425,6 @@
 
     @Test
     public void testSetProxyDisallowedApps() throws Exception {
-        assumeTrue(supportedHardware());
         ProxyInfo initialProxy = mCM.getDefaultProxy();
 
         String disallowedApps = mPackageName;
@@ -1441,7 +1450,6 @@
 
     @Test
     public void testNoProxy() throws Exception {
-        assumeTrue(supportedHardware());
         ProxyInfo initialProxy = mCM.getDefaultProxy();
         BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
         proxyBroadcastReceiver.register();
@@ -1476,7 +1484,6 @@
 
     @Test
     public void testBindToNetworkWithProxy() throws Exception {
-        assumeTrue(supportedHardware());
         String allowedApps = mPackageName;
         Network initialNetwork = mCM.getActiveNetwork();
         ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -1501,9 +1508,6 @@
 
     @Test
     public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
-        if (!supportedHardware()) {
-            return;
-        }
         // VPN is not routing any traffic i.e. its underlying networks is an empty array.
         ArrayList<Network> underlyingNetworks = new ArrayList<>();
         String allowedApps = mPackageName;
@@ -1533,9 +1537,6 @@
 
     @Test
     public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
-        if (!supportedHardware()) {
-            return;
-        }
         Network underlyingNetwork = mCM.getActiveNetwork();
         if (underlyingNetwork == null) {
             Log.i(TAG, "testVpnMeterednessWithNullUnderlyingNetwork cannot execute"
@@ -1562,9 +1563,6 @@
 
     @Test
     public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
-        if (!supportedHardware()) {
-            return;
-        }
         Network underlyingNetwork = mCM.getActiveNetwork();
         if (underlyingNetwork == null) {
             Log.i(TAG, "testVpnMeterednessWithNonNullUnderlyingNetwork cannot execute"
@@ -1604,9 +1602,6 @@
 
     @Test
     public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
-        if (!supportedHardware()) {
-            return;
-        }
         Network underlyingNetwork = mCM.getActiveNetwork();
         if (underlyingNetwork == null) {
             Log.i(TAG, "testAlwaysMeteredVpnWithNullUnderlyingNetwork cannot execute"
@@ -1631,9 +1626,6 @@
 
     @Test
     public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
-        if (!supportedHardware()) {
-            return;
-        }
         Network underlyingNetwork = mCM.getActiveNetwork();
         if (underlyingNetwork == null) {
             Log.i(TAG, "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork cannot execute"
@@ -1671,9 +1663,6 @@
 
     @Test
     public void testB141603906() throws Exception {
-        if (!supportedHardware()) {
-            return;
-        }
         final InetSocketAddress src = new InetSocketAddress(0);
         final InetSocketAddress dst = new InetSocketAddress(0);
         final int NUM_THREADS = 8;
@@ -1781,8 +1770,6 @@
      */
     @Test
     public void testDownloadWithDownloadManagerDisallowed() throws Exception {
-        assumeTrue(supportedHardware());
-
         // Start a VPN with DownloadManager package in disallowed list.
         startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
                 new String[] {"192.0.2.0/24", "2001:db8::/32"},
@@ -1838,7 +1825,6 @@
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testBlockIncomingPackets() throws Exception {
-        assumeTrue(supportedHardware());
         final Network network = mCM.getActiveNetwork();
         assertNotNull("Requires a working Internet connection", network);
 
@@ -1907,7 +1893,6 @@
 
     @Test
     public void testSetVpnDefaultForUids() throws Exception {
-        assumeTrue(supportedHardware());
         assumeTrue(SdkLevel.isAtLeastU());
 
         final Network defaultNetwork = mCM.getActiveNetwork();
@@ -1953,6 +1938,81 @@
             });
     }
 
+    /**
+     * Check if packets to a VPN interface's IP arriving on a non-VPN interface are dropped or not.
+     * If the test interface has a different address from the VPN interface, packets must be dropped
+     * If the test interface has the same address as the VPN interface, packets must not be
+     * dropped
+     *
+     * @param duplicatedAddress true to bring up the test interface with the same address as the VPN
+     *                          interface
+     */
+    private void doTestDropPacketToVpnAddress(final boolean duplicatedAddress)
+            throws Exception {
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .addTransportType(TRANSPORT_TEST)
+                .build();
+        final CtsNetUtils.TestNetworkCallback callback = new CtsNetUtils.TestNetworkCallback();
+        mCM.requestNetwork(request, callback);
+        final ParcelFileDescriptor srcTunFd = runWithShellPermissionIdentity(() -> {
+            final TestNetworkManager tnm = mTestContext.getSystemService(TestNetworkManager.class);
+            List<LinkAddress> linkAddresses = duplicatedAddress
+                    ? List.of(new LinkAddress("192.0.2.2/24"),
+                            new LinkAddress("2001:db8:1:2::ffe/64")) :
+                    List.of(new LinkAddress("198.51.100.2/24"),
+                            new LinkAddress("2001:db8:3:4::ffe/64"));
+            final TestNetworkInterface iface = tnm.createTunInterface(linkAddresses);
+            tnm.setupTestNetwork(iface.getInterfaceName(), new Binder());
+            return iface.getFileDescriptor();
+        }, MANAGE_TEST_NETWORKS);
+        final Network testNetwork = callback.waitForAvailable();
+        assertNotNull(testNetwork);
+        final DatagramSocket dstSock = new DatagramSocket();
+
+        testAndCleanup(() -> {
+            startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+                    new String[]{"0.0.0.0/0", "::/0"} /* routes */,
+                    "" /* allowedApplications */, "" /* disallowedApplications */,
+                    null /* proxyInfo */, null /* underlyingNetworks */,
+                    false /* isAlwaysMetered */);
+
+            final FileDescriptor dstUdpFd = dstSock.getFileDescriptor$();
+            checkBlockUdp(srcTunFd.getFileDescriptor(), dstUdpFd,
+                    InetAddresses.parseNumericAddress("192.0.2.2") /* dstAddress */,
+                    InetAddresses.parseNumericAddress("192.0.2.1") /* srcAddress */,
+                    duplicatedAddress ? EXPECT_PASS : EXPECT_BLOCK);
+            checkBlockUdp(srcTunFd.getFileDescriptor(), dstUdpFd,
+                    InetAddresses.parseNumericAddress("2001:db8:1:2::ffe") /* dstAddress */,
+                    InetAddresses.parseNumericAddress("2001:db8:1:2::ffa") /* srcAddress */,
+                    duplicatedAddress ? EXPECT_PASS : EXPECT_BLOCK);
+
+            // Traffic on VPN should not be affected
+            checkTrafficOnVpn();
+        }, /* cleanup */ () -> {
+                srcTunFd.close();
+                dstSock.close();
+            }, /* cleanup */ () -> {
+                runWithShellPermissionIdentity(() -> {
+                    mTestContext.getSystemService(TestNetworkManager.class)
+                            .teardownTestNetwork(testNetwork);
+                }, MANAGE_TEST_NETWORKS);
+            }, /* cleanup */ () -> {
+                mCM.unregisterNetworkCallback(callback);
+            });
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testDropPacketToVpnAddress_WithoutDuplicatedAddress() throws Exception {
+        doTestDropPacketToVpnAddress(false /* duplicatedAddress */);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testDropPacketToVpnAddress_WithDuplicatedAddress() throws Exception {
+        doTestDropPacketToVpnAddress(true /* duplicatedAddress */);
+    }
+
     private ByteBuffer buildIpv4UdpPacket(final Inet4Address dstAddr, final Inet4Address srcAddr,
             final short dstPort, final short srcPort, final byte[] payload) throws IOException {
 
@@ -1996,7 +2056,8 @@
     private void checkBlockUdp(
             final FileDescriptor srcTunFd,
             final FileDescriptor dstUdpFd,
-            final boolean ipv6,
+            final InetAddress dstAddress,
+            final InetAddress srcAddress,
             final boolean expectBlock) throws Exception {
         final Random random = new Random();
         final byte[] sendData = new byte[100];
@@ -2004,15 +2065,15 @@
         final short dstPort = (short) ((InetSocketAddress) Os.getsockname(dstUdpFd)).getPort();
 
         ByteBuffer buf;
-        if (ipv6) {
+        if (dstAddress instanceof Inet6Address) {
             buf = buildIpv6UdpPacket(
-                    (Inet6Address) TEST_IP6_DST_ADDR.getAddress(),
-                    (Inet6Address) TEST_IP6_SRC_ADDR.getAddress(),
+                    (Inet6Address) dstAddress,
+                    (Inet6Address) srcAddress,
                     dstPort, TEST_SRC_PORT, sendData);
         } else {
             buf = buildIpv4UdpPacket(
-                    (Inet4Address) TEST_IP4_DST_ADDR.getAddress(),
-                    (Inet4Address) TEST_IP4_SRC_ADDR.getAddress(),
+                    (Inet4Address) dstAddress,
+                    (Inet4Address) srcAddress,
                     dstPort, TEST_SRC_PORT, sendData);
         }
 
@@ -2038,8 +2099,10 @@
             final FileDescriptor srcTunFd,
             final FileDescriptor dstUdpFd,
             final boolean expectBlock) throws Exception {
-        checkBlockUdp(srcTunFd, dstUdpFd, false /* ipv6 */, expectBlock);
-        checkBlockUdp(srcTunFd, dstUdpFd, true /* ipv6 */, expectBlock);
+        checkBlockUdp(srcTunFd, dstUdpFd, TEST_IP4_DST_ADDR.getAddress(),
+                TEST_IP4_SRC_ADDR.getAddress(), expectBlock);
+        checkBlockUdp(srcTunFd, dstUdpFd, TEST_IP6_DST_ADDR.getAddress(),
+                TEST_IP6_SRC_ADDR.getAddress(), expectBlock);
     }
 
     private class DetailedBlockedStatusCallback extends TestableNetworkCallback {
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index c526172..12ea23b 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -35,5 +35,5 @@
         "general-tests",
         "sts",
     ],
-    certificate: ":cts-net-app",
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
index 2c2d957..412b307 100644
--- a/tests/cts/hostside/app2/AndroidManifest.xml
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -40,33 +40,8 @@
     <application android:usesCleartextTraffic="true"
             android:testOnly="true"
             android:debuggable="true">
-
-        <activity android:name=".MyActivity"
-             android:exported="true"/>
-        <service android:name=".MyService"
-             android:exported="true"/>
-        <service android:name=".MyForegroundService"
-             android:foregroundServiceType="specialUse"
-             android:exported="true">
-            <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" 
-                      android:value="Connectivity" />
-        </service>
         <service android:name=".RemoteSocketFactoryService"
              android:exported="true"/>
-
-        <receiver android:name=".MyBroadcastReceiver"
-             android:exported="true">
-            <intent-filter>
-                <action android:name="android.net.conn.RESTRICT_BACKGROUND_CHANGED"/>
-                <action android:name="com.android.cts.net.hostside.app2.action.GET_COUNTERS"/>
-                <action android:name="com.android.cts.net.hostside.app2.action.GET_RESTRICT_BACKGROUND_STATUS"/>
-                <action android:name="com.android.cts.net.hostside.app2.action.CHECK_NETWORK"/>
-                <action android:name="com.android.cts.net.hostside.app2.action.SEND_NOTIFICATION"/>
-                <action android:name="com.android.cts.net.hostside.app2.action.SHOW_TOAST"/>
-                </intent-filter>
-        </receiver>
-        <service android:name=".MyJobService"
-            android:permission="android.permission.BIND_JOB_SERVICE" />
     </application>
 
     <!--
diff --git a/tests/cts/hostside/app2/res/drawable/ic_notification.png b/tests/cts/hostside/app2/res/drawable/ic_notification.png
deleted file mode 100644
index 6ae570b..0000000
--- a/tests/cts/hostside/app2/res/drawable/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
deleted file mode 100644
index 37dc7a0..0000000
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside.app2;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.net.hostside.INetworkStateObserver;
-
-public final class Common {
-
-    static final String TAG = "CtsNetApp2";
-
-    // Constants below must match values defined on app's
-    // AbstractRestrictBackgroundNetworkTestCase.java
-    static final String MANIFEST_RECEIVER = "ManifestReceiver";
-    static final String DYNAMIC_RECEIVER = "DynamicReceiver";
-
-    static final String ACTION_RECEIVER_READY =
-            "com.android.cts.net.hostside.app2.action.RECEIVER_READY";
-    static final String ACTION_FINISH_ACTIVITY =
-            "com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY";
-    static final String ACTION_FINISH_JOB =
-            "com.android.cts.net.hostside.app2.action.FINISH_JOB";
-    static final String ACTION_SHOW_TOAST =
-            "com.android.cts.net.hostside.app2.action.SHOW_TOAST";
-    // Copied from com.android.server.net.NetworkPolicyManagerService class
-    static final String ACTION_SNOOZE_WARNING =
-            "com.android.server.net.action.SNOOZE_WARNING";
-
-    static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
-    static final String NOTIFICATION_TYPE_DELETE = "DELETE";
-    static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
-    static final String NOTIFICATION_TYPE_BUNDLE = "BUNDLE";
-    static final String NOTIFICATION_TYPE_ACTION = "ACTION";
-    static final String NOTIFICATION_TYPE_ACTION_BUNDLE = "ACTION_BUNDLE";
-    static final String NOTIFICATION_TYPE_ACTION_REMOTE_INPUT = "ACTION_REMOTE_INPUT";
-
-    static final String TEST_PKG = "com.android.cts.net.hostside";
-    static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
-    static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
-
-    static final int TYPE_COMPONENT_ACTIVTY = 0;
-    static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
-    static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
-
-    static int getUid(Context context) {
-        final String packageName = context.getPackageName();
-        try {
-            return context.getPackageManager().getPackageUid(packageName, 0);
-        } catch (NameNotFoundException e) {
-            throw new IllegalStateException("Could not get UID for " + packageName, e);
-        }
-    }
-
-    private static boolean validateComponentState(Context context, int componentType,
-            INetworkStateObserver observer) throws RemoteException {
-        final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
-        switch (componentType) {
-            case TYPE_COMPONENT_ACTIVTY: {
-                final int procState = activityManager.getUidProcessState(Process.myUid());
-                if (procState != ActivityManager.PROCESS_STATE_TOP) {
-                    observer.onNetworkStateChecked(
-                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
-                            "Unexpected procstate: " + procState);
-                    return false;
-                }
-                return true;
-            }
-            case TYPE_COMPONENT_FOREGROUND_SERVICE: {
-                final int procState = activityManager.getUidProcessState(Process.myUid());
-                if (procState != ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
-                    observer.onNetworkStateChecked(
-                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
-                            "Unexpected procstate: " + procState);
-                    return false;
-                }
-                return true;
-            }
-            case TYPE_COMPONENT_EXPEDITED_JOB: {
-                final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid());
-                if ((capabilities
-                        & ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) == 0) {
-                    observer.onNetworkStateChecked(
-                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES,
-                            "Unexpected capabilities: " + capabilities);
-                    return false;
-                }
-                return true;
-            }
-            default: {
-                observer.onNetworkStateChecked(INetworkStateObserver.RESULT_ERROR_OTHER,
-                        "Unknown component type: " + componentType);
-                return false;
-            }
-        }
-    }
-
-    static void notifyNetworkStateObserver(Context context, Intent intent, int componentType) {
-        if (intent == null) {
-            return;
-        }
-        final Bundle extras = intent.getExtras();
-        notifyNetworkStateObserver(context, extras, componentType);
-    }
-
-    static void notifyNetworkStateObserver(Context context, Bundle extras, int componentType) {
-        if (extras == null) {
-            return;
-        }
-        final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface(
-                extras.getBinder(KEY_NETWORK_STATE_OBSERVER));
-        if (observer != null) {
-            try {
-                final boolean skipValidation = extras.getBoolean(KEY_SKIP_VALIDATION_CHECKS);
-                if (!skipValidation && !validateComponentState(context, componentType, observer)) {
-                    return;
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "Error occurred while informing the validation result: " + e);
-            }
-            AsyncTask.execute(() -> {
-                try {
-                    observer.onNetworkStateChecked(
-                            INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED,
-                            MyBroadcastReceiver.checkNetworkStatus(context));
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Error occurred while notifying the observer: " + e);
-                }
-            });
-        }
-    }
-}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
deleted file mode 100644
index aa58ff9..0000000
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside.app2;
-
-import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_ACTIVITY;
-import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_ACTIVTY;
-
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.os.RemoteCallback;
-import android.util.Log;
-import android.view.WindowManager;
-
-import androidx.annotation.GuardedBy;
-
-/**
- * Activity used to bring process to foreground.
- */
-public class MyActivity extends Activity {
-
-    @GuardedBy("this")
-    private BroadcastReceiver finishCommandReceiver = null;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        Log.d(TAG, "MyActivity.onCreate()");
-
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-    }
-
-    @Override
-    public void finish() {
-        synchronized (this) {
-            if (finishCommandReceiver != null) {
-                unregisterReceiver(finishCommandReceiver);
-                finishCommandReceiver = null;
-            }
-        }
-        super.finish();
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        Log.d(TAG, "MyActivity.onStart()");
-    }
-
-    @Override
-    protected void onNewIntent(Intent intent) {
-        super.onNewIntent(intent);
-        Log.d(TAG, "MyActivity.onNewIntent()");
-        setIntent(intent);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        Log.d(TAG, "MyActivity.onResume(): " + getIntent());
-        Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
-        synchronized (this) {
-            finishCommandReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    Log.d(TAG, "Finishing MyActivity");
-                    MyActivity.this.finish();
-                }
-            };
-            registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
-                    Context.RECEIVER_EXPORTED);
-        }
-        final RemoteCallback callback = getIntent().getParcelableExtra(
-                Intent.EXTRA_REMOTE_CALLBACK);
-        if (callback != null) {
-            callback.sendResult(null);
-        }
-    }
-
-    @Override
-    protected void onDestroy() {
-        Log.d(TAG, "MyActivity.onDestroy()");
-        super.onDestroy();
-    }
-}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
deleted file mode 100644
index 825f2c9..0000000
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
+++ /dev/null
@@ -1,277 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside.app2;
-
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-
-import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY;
-import static com.android.cts.net.hostside.app2.Common.ACTION_SHOW_TOAST;
-import static com.android.cts.net.hostside.app2.Common.ACTION_SNOOZE_WARNING;
-import static com.android.cts.net.hostside.app2.Common.MANIFEST_RECEIVER;
-import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION;
-import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_BUNDLE;
-import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_ACTION_REMOTE_INPUT;
-import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_BUNDLE;
-import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_CONTENT;
-import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
-import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
-import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.cts.net.hostside.app2.Common.getUid;
-
-import android.app.Notification;
-import android.app.Notification.Action;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.RemoteInput;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.Bundle;
-import android.util.Log;
-import android.widget.Toast;
-
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.URL;
-
-/**
- * Receiver used to:
- * <ol>
- *   <li>Count number of {@code RESTRICT_BACKGROUND_CHANGED} broadcasts received.
- *   <li>Show a toast.
- * </ol>
- */
-public class MyBroadcastReceiver extends BroadcastReceiver {
-
-    private static final int NETWORK_TIMEOUT_MS = 5 * 1000;
-
-    private final String mName;
-
-    public MyBroadcastReceiver() {
-        this(MANIFEST_RECEIVER);
-    }
-
-    MyBroadcastReceiver(String name) {
-        Log.d(TAG, "Constructing MyBroadcastReceiver named " + name);
-        mName = name;
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Log.d(TAG, "onReceive() for " + mName + ": " + intent);
-        final String action = intent.getAction();
-        switch (action) {
-            case ACTION_SNOOZE_WARNING:
-                increaseCounter(context, action);
-                break;
-            case ACTION_RESTRICT_BACKGROUND_CHANGED:
-                increaseCounter(context, action);
-                break;
-            case ACTION_RECEIVER_READY:
-                final String message = mName + " is ready to rumble";
-                Log.d(TAG, message);
-                setResultData(message);
-                break;
-            case ACTION_SHOW_TOAST:
-                showToast(context);
-                break;
-            default:
-                Log.e(TAG, "received unexpected action: " + action);
-        }
-    }
-
-    @Override
-    public String toString() {
-        return "[MyBroadcastReceiver: mName=" + mName + "]";
-    }
-
-    private void increaseCounter(Context context, String action) {
-        final SharedPreferences prefs = context.getApplicationContext()
-                .getSharedPreferences(mName, Context.MODE_PRIVATE);
-        final int value = prefs.getInt(action, 0) + 1;
-        Log.d(TAG, "increaseCounter('" + action + "'): setting '" + mName + "' to " + value);
-        prefs.edit().putInt(action, value).apply();
-    }
-
-    static int getCounter(Context context, String action, String receiverName) {
-        final SharedPreferences prefs = context.getSharedPreferences(receiverName,
-                Context.MODE_PRIVATE);
-        final int value = prefs.getInt(action, 0);
-        Log.d(TAG, "getCounter('" + action + "', '" + receiverName + "'): " + value);
-        return value;
-    }
-
-    static String getRestrictBackgroundStatus(Context context) {
-        final ConnectivityManager cm = (ConnectivityManager) context
-                .getSystemService(Context.CONNECTIVITY_SERVICE);
-        final int apiStatus = cm.getRestrictBackgroundStatus();
-        Log.d(TAG, "getRestrictBackgroundStatus: returning " + apiStatus);
-        return String.valueOf(apiStatus);
-    }
-
-    private static final String NETWORK_STATUS_TEMPLATE = "%s|%s|%s|%s|%s";
-    /**
-     * Checks whether the network is available and return a string which can then be send as a
-     * result data for the ordered broadcast.
-     *
-     * <p>
-     * The string has the following format:
-     *
-     * <p><pre><code>
-     * NetinfoState|NetinfoDetailedState|RealConnectionCheck|RealConnectionCheckDetails|Netinfo
-     * </code></pre>
-     *
-     * <p>Where:
-     *
-     * <ul>
-     * <li>{@code NetinfoState}: enum value of {@link NetworkInfo.State}.
-     * <li>{@code NetinfoDetailedState}: enum value of {@link NetworkInfo.DetailedState}.
-     * <li>{@code RealConnectionCheck}: boolean value of a real connection check (i.e., an attempt
-     *     to access an external website.
-     * <li>{@code RealConnectionCheckDetails}: if HTTP output core or exception string of the real
-     *     connection attempt
-     * <li>{@code Netinfo}: string representation of the {@link NetworkInfo}.
-     * </ul>
-     *
-     * For example, if the connection was established fine, the result would be something like:
-     * <p><pre><code>
-     * CONNECTED|CONNECTED|true|200|[type: WIFI[], state: CONNECTED/CONNECTED, reason: ...]
-     * </code></pre>
-     *
-     */
-    // TODO: now that it uses Binder, it counl return a Bundle with the data parts instead...
-    static String checkNetworkStatus(Context context) {
-        final ConnectivityManager cm =
-                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        // TODO: connect to a hostside server instead
-        final String address = "http://example.com";
-        final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
-        Log.d(TAG, "Running checkNetworkStatus() on thread "
-                + Thread.currentThread().getName() + " for UID " + getUid(context)
-                + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
-        boolean checkStatus = false;
-        String checkDetails = "N/A";
-        try {
-            final URL url = new URL(address);
-            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
-            conn.setReadTimeout(NETWORK_TIMEOUT_MS);
-            conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
-            conn.setRequestMethod("GET");
-            conn.setDoInput(true);
-            conn.connect();
-            final int response = conn.getResponseCode();
-            checkStatus = true;
-            checkDetails = "HTTP response for " + address + ": " + response;
-        } catch (Exception e) {
-            checkStatus = false;
-            checkDetails = "Exception getting " + address + ": " + e;
-        }
-        // If the app tries to make a network connection in the foreground immediately after
-        // trying to do the same when it's network access was blocked, it could receive a
-        // UnknownHostException due to the cached DNS entry. So, clear the dns cache after
-        // every network access for now until we have a fix on the platform side.
-        InetAddress.clearDnsCache();
-        Log.d(TAG, checkDetails);
-        final String state, detailedState;
-        if (networkInfo != null) {
-            state = networkInfo.getState().name();
-            detailedState = networkInfo.getDetailedState().name();
-        } else {
-            state = detailedState = "null";
-        }
-        final String status = String.format(NETWORK_STATUS_TEMPLATE, state, detailedState,
-                Boolean.valueOf(checkStatus), checkDetails, networkInfo);
-        Log.d(TAG, "Offering " + status);
-        return status;
-    }
-
-    /**
-     * Sends a system notification containing actions with pending intents to launch the app's
-     * main activitiy or service.
-     */
-    static void sendNotification(Context context, String channelId, int notificationId,
-            String notificationType ) {
-        Log.d(TAG, "sendNotification: id=" + notificationId + ", type=" + notificationType);
-        final Intent serviceIntent = new Intent(context, MyService.class);
-        final PendingIntent pendingIntent = PendingIntent.getService(context, 0, serviceIntent,
-                PendingIntent.FLAG_MUTABLE);
-        final Bundle bundle = new Bundle();
-        bundle.putCharSequence("parcelable", "I am not");
-
-        final Notification.Builder builder = new Notification.Builder(context, channelId)
-                .setSmallIcon(R.drawable.ic_notification);
-
-        Action action = null;
-        switch (notificationType) {
-            case NOTIFICATION_TYPE_CONTENT:
-                builder
-                    .setContentTitle("Light, Cameras...")
-                    .setContentIntent(pendingIntent);
-                break;
-            case NOTIFICATION_TYPE_DELETE:
-                builder.setDeleteIntent(pendingIntent);
-                break;
-            case NOTIFICATION_TYPE_FULL_SCREEN:
-                builder.setFullScreenIntent(pendingIntent, true);
-                break;
-            case NOTIFICATION_TYPE_BUNDLE:
-                bundle.putParcelable("Magnum P.I. (Pending Intent)", pendingIntent);
-                builder.setExtras(bundle);
-                break;
-            case NOTIFICATION_TYPE_ACTION:
-                action = new Action.Builder(
-                        R.drawable.ic_notification, "ACTION", pendingIntent)
-                        .build();
-                builder.addAction(action);
-                break;
-            case NOTIFICATION_TYPE_ACTION_BUNDLE:
-                bundle.putParcelable("Magnum A.P.I. (Action Pending Intent)", pendingIntent);
-                action = new Action.Builder(
-                        R.drawable.ic_notification, "ACTION WITH BUNDLE", null)
-                        .addExtras(bundle)
-                        .build();
-                builder.addAction(action);
-                break;
-            case NOTIFICATION_TYPE_ACTION_REMOTE_INPUT:
-                bundle.putParcelable("Magnum R.I. (Remote Input)", null);
-                final RemoteInput remoteInput = new RemoteInput.Builder("RI")
-                    .addExtras(bundle)
-                    .build();
-                action = new Action.Builder(
-                        R.drawable.ic_notification, "ACTION WITH REMOTE INPUT", pendingIntent)
-                        .addRemoteInput(remoteInput)
-                        .build();
-                builder.addAction(action);
-                break;
-            default:
-                Log.e(TAG, "Unknown notification type: " + notificationType);
-                return;
-        }
-
-        final Notification notification = builder.build();
-        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
-            .notify(notificationId, notification);
-    }
-
-    private void showToast(Context context) {
-        Toast.makeText(context, "Toast from CTS test", Toast.LENGTH_SHORT).show();
-        setResultData("Shown");
-    }
-}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
deleted file mode 100644
index b55761c..0000000
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside.app2;
-
-import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.cts.net.hostside.app2.Common.TEST_PKG;
-import static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_FOREGROUND_SERVICE;
-
-import android.R;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.net.hostside.INetworkStateObserver;
-
-/**
- * Service used to change app state to FOREGROUND_SERVICE.
- */
-public class MyForegroundService extends Service {
-    private static final String NOTIFICATION_CHANNEL_ID = "cts/MyForegroundService";
-    private static final int FLAG_START_FOREGROUND = 1;
-    private static final int FLAG_STOP_FOREGROUND = 2;
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return null;
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        Log.v(TAG, "MyForegroundService.onStartCommand(): " + intent);
-        NotificationManager notificationManager = getSystemService(NotificationManager.class);
-        notificationManager.createNotificationChannel(new NotificationChannel(
-                NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
-                NotificationManager.IMPORTANCE_DEFAULT));
-        switch (intent.getFlags()) {
-            case FLAG_START_FOREGROUND:
-                Log.d(TAG, "Starting foreground");
-                startForeground(42, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
-                        .setSmallIcon(R.drawable.ic_dialog_alert) // any icon is fine
-                        .build());
-                Common.notifyNetworkStateObserver(this, intent, TYPE_COMPONENT_FOREGROUND_SERVICE);
-                break;
-            case FLAG_STOP_FOREGROUND:
-                Log.d(TAG, "Stopping foreground");
-                stopForeground(true);
-                break;
-            default:
-                Log.wtf(TAG, "Invalid flag on intent " + intent);
-        }
-        return START_STICKY;
-    }
-}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
deleted file mode 100644
index 8c112b6..0000000
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.cts.net.hostside.app2;
-
-import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_JOB;
-import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_EXPEDITED_JOB;
-
-import android.app.job.JobParameters;
-import android.app.job.JobService;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.Log;
-
-public class MyJobService extends JobService {
-
-    private BroadcastReceiver mFinishCommandReceiver = null;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        Log.v(TAG, "MyJobService.onCreate()");
-    }
-
-    @Override
-    public boolean onStartJob(JobParameters params) {
-        Log.v(TAG, "MyJobService.onStartJob()");
-        Common.notifyNetworkStateObserver(this, params.getTransientExtras(),
-                TYPE_COMPONENT_EXPEDITED_JOB);
-        mFinishCommandReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                Log.v(TAG, "Finishing MyJobService");
-                try {
-                    jobFinished(params, /*wantsReschedule=*/ false);
-                } finally {
-                    if (mFinishCommandReceiver != null) {
-                        unregisterReceiver(mFinishCommandReceiver);
-                        mFinishCommandReceiver = null;
-                    }
-                }
-            }
-        };
-        registerReceiver(mFinishCommandReceiver, new IntentFilter(ACTION_FINISH_JOB),
-                Context.RECEIVER_EXPORTED);
-        return true;
-    }
-
-    @Override
-    public boolean onStopJob(JobParameters params) {
-        // If this job is stopped before it had a chance to send network status via
-        // INetworkStateObserver, the test will fail. It could happen either due to test timing out
-        // or this app moving to a lower proc_state and losing network access.
-        Log.v(TAG, "MyJobService.onStopJob()");
-        if (mFinishCommandReceiver != null) {
-            unregisterReceiver(mFinishCommandReceiver);
-            mFinishCommandReceiver = null;
-        }
-        return false;
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        Log.v(TAG, "MyJobService.onDestroy()");
-    }
-}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
deleted file mode 100644
index 3ed5391..0000000
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net.hostside.app2;
-
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-
-import static com.android.cts.net.hostside.app2.Common.ACTION_RECEIVER_READY;
-import static com.android.cts.net.hostside.app2.Common.ACTION_SNOOZE_WARNING;
-import static com.android.cts.net.hostside.app2.Common.DYNAMIC_RECEIVER;
-import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
-
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.cts.net.hostside.IMyService;
-import com.android.cts.net.hostside.INetworkCallback;
-import com.android.modules.utils.build.SdkLevel;
-
-/**
- * Service used to dynamically register a broadcast receiver.
- */
-public class MyService extends Service {
-    private static final String NOTIFICATION_CHANNEL_ID = "MyService";
-
-    ConnectivityManager mCm;
-
-    private MyBroadcastReceiver mReceiver;
-    private ConnectivityManager.NetworkCallback mNetworkCallback;
-
-    // TODO: move MyBroadcast static functions here - they were kept there to make git diff easier.
-
-    private IMyService.Stub mBinder =
-        new IMyService.Stub() {
-
-        @Override
-        public void registerBroadcastReceiver() {
-            if (mReceiver != null) {
-                Log.d(TAG, "receiver already registered: " + mReceiver);
-                return;
-            }
-            final Context context = getApplicationContext();
-            final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
-            mReceiver = new MyBroadcastReceiver(DYNAMIC_RECEIVER);
-            context.registerReceiver(mReceiver,
-                    new IntentFilter(ACTION_RECEIVER_READY), flags);
-            context.registerReceiver(mReceiver,
-                    new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED), flags);
-            context.registerReceiver(mReceiver,
-                    new IntentFilter(ACTION_SNOOZE_WARNING), flags);
-            Log.d(TAG, "receiver registered");
-        }
-
-        @Override
-        public int getCounters(String receiverName, String action) {
-            return MyBroadcastReceiver.getCounter(getApplicationContext(), action, receiverName);
-        }
-
-        @Override
-        public String checkNetworkStatus() {
-            return MyBroadcastReceiver.checkNetworkStatus(getApplicationContext());
-        }
-
-        @Override
-        public String getRestrictBackgroundStatus() {
-            return MyBroadcastReceiver.getRestrictBackgroundStatus(getApplicationContext());
-        }
-
-        @Override
-        public void sendNotification(int notificationId, String notificationType) {
-            MyBroadcastReceiver .sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
-                    notificationId, notificationType);
-        }
-
-        @Override
-        public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb) {
-            if (mNetworkCallback != null) {
-                Log.d(TAG, "unregister previous network callback: " + mNetworkCallback);
-                unregisterNetworkCallback();
-            }
-            Log.d(TAG, "registering network callback for " + request);
-
-            mNetworkCallback = new ConnectivityManager.NetworkCallback() {
-                @Override
-                public void onBlockedStatusChanged(Network network, boolean blocked) {
-                    try {
-                        cb.onBlockedStatusChanged(network, blocked);
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Cannot send onBlockedStatusChanged: " + e);
-                        unregisterNetworkCallback();
-                    }
-                }
-
-                @Override
-                public void onAvailable(Network network) {
-                    try {
-                        cb.onAvailable(network);
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Cannot send onAvailable: " + e);
-                        unregisterNetworkCallback();
-                    }
-                }
-
-                @Override
-                public void onLost(Network network) {
-                    try {
-                        cb.onLost(network);
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Cannot send onLost: " + e);
-                        unregisterNetworkCallback();
-                    }
-                }
-
-                @Override
-                public void onCapabilitiesChanged(Network network, NetworkCapabilities cap) {
-                    try {
-                        cb.onCapabilitiesChanged(network, cap);
-                    } catch (RemoteException e) {
-                        Log.d(TAG, "Cannot send onCapabilitiesChanged: " + e);
-                        unregisterNetworkCallback();
-                    }
-                }
-            };
-            mCm.registerNetworkCallback(request, mNetworkCallback);
-            try {
-                cb.asBinder().linkToDeath(() -> unregisterNetworkCallback(), 0);
-            } catch (RemoteException e) {
-                unregisterNetworkCallback();
-            }
-        }
-
-        @Override
-        public void unregisterNetworkCallback() {
-            Log.d(TAG, "unregistering network callback");
-            if (mNetworkCallback != null) {
-                mCm.unregisterNetworkCallback(mNetworkCallback);
-                mNetworkCallback = null;
-            }
-        }
-
-        @Override
-        public int scheduleJob(JobInfo jobInfo) {
-            final JobScheduler jobScheduler = getApplicationContext()
-                    .getSystemService(JobScheduler.class);
-            return jobScheduler.schedule(jobInfo);
-        }
-      };
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
-    }
-
-    @Override
-    public void onCreate() {
-        final Context context = getApplicationContext();
-        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
-                .createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID,
-                        NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
-        mCm = (ConnectivityManager) getApplicationContext()
-                .getSystemService(Context.CONNECTIVITY_SERVICE);
-    }
-
-    @Override
-    public void onDestroy() {
-        final Context context = getApplicationContext();
-        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
-                .deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
-        if (mReceiver != null) {
-            Log.d(TAG, "onDestroy(): unregistering " + mReceiver);
-            getApplicationContext().unregisterReceiver(mReceiver);
-        }
-
-        super.onDestroy();
-    }
-}
diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp
deleted file mode 100644
index 301973e..0000000
--- a/tests/cts/hostside/certs/Android.bp
+++ /dev/null
@@ -1,9 +0,0 @@
-package {
-    default_team: "trendy_team_fwk_core_networking",
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-android_app_certificate {
-    name: "cts-net-app",
-    certificate: "cts-net-app",
-}
diff --git a/tests/cts/hostside/certs/README b/tests/cts/hostside/certs/README
deleted file mode 100644
index b660a82..0000000
--- a/tests/cts/hostside/certs/README
+++ /dev/null
@@ -1,2 +0,0 @@
-# Generated with:
-development/tools/make_key cts-net-app '/CN=cts-net-app'
diff --git a/tests/cts/hostside/certs/cts-net-app.pk8 b/tests/cts/hostside/certs/cts-net-app.pk8
deleted file mode 100644
index 1703e4e..0000000
--- a/tests/cts/hostside/certs/cts-net-app.pk8
+++ /dev/null
Binary files differ
diff --git a/tests/cts/hostside/certs/cts-net-app.x509.pem b/tests/cts/hostside/certs/cts-net-app.x509.pem
deleted file mode 100644
index a15ff48..0000000
--- a/tests/cts/hostside/certs/cts-net-app.x509.pem
+++ /dev/null
@@ -1,19 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDAjCCAeqgAwIBAgIJAMhWwIIqr1r6MA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
-BAMMC2N0cy1uZXQtYXBwMB4XDTE4MDYyMDAyMjAwN1oXDTQ1MTEwNTAyMjAwN1ow
-FjEUMBIGA1UEAwwLY3RzLW5ldC1hcHAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
-ggEKAoIBAQDefOayWQss1E+FQIONK6IhlXhe0BEyHshIrnPOOmuCPa/Svfbnmziy
-hr1KTjaQ3ET/mGShwlt6AUti7nKx9aB71IJp5mSBuwW62A8jvN3yNOo45YV8+n1o
-TrEoMWMf7hQmoOSqaSJ+VFuVms/kPSEh99okDgHCej6rsEkEcDoh6pJajQyUYDwR
-SNAF8SrqCDhqFbZW/LWedvuikCUlNtzuv7/GrcLcsiWEfHv7UOBKpMjLo9BhD1XF
-IefnxImcBQrQGMnE9TLixBiEeX5yauLgbZuxBqD/zsI2TH1FjxTeuJan83kLbqqH
-FgyvPaUjwckAdQPyom7ZUYFnBc0LQ9xzAgMBAAGjUzBRMB0GA1UdDgQWBBRZrBEw
-tAB2WNXj8dQ7ZOuJ34kY5DAfBgNVHSMEGDAWgBRZrBEwtAB2WNXj8dQ7ZOuJ34kY
-5DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDeI9AnLW6l/39y
-z96w/ldxZVFPzBRiFIsJsPHVyXlD5vUHZv/ju2jFn8TZSZR5TK0bzCEoVLp34Sho
-bbS0magP82yIvCRibyoyD+TDNnZkNJwjYnikE+/oyshTSQtpkn/rDA+0Y09BUC1E
-N2I6bV9pTXLFg7oah2FmqPRPzhgeYUKENgOQkrrjUCn6y0i/k374n7aftzdniSIz
-2kCRVEeN9gws6CnoMPx0vr32v/JVuPV6zfdJYadgj/eFRyTNE4msd9kE82Wc46eU
-YiI+LuXZ3ZMUNWGY7MK2pOUUS52JsBQ3K235dA5WaU4x8OBlY/WkNYX/eLbNs5jj
-FzLmhZZ1
------END CERTIFICATE-----
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index 100b6e4..c220000 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -44,6 +44,7 @@
         "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithoutProperty.xml",
+    sdk_version: "test_current",
 }
 
 android_test_helper_app {
@@ -53,6 +54,7 @@
         "CtsHostsideNetworkCapTestsAppDefaults",
     ],
     manifest: "AndroidManifestWithProperty.xml",
+    sdk_version: "test_current",
 }
 
 android_test_helper_app {
@@ -63,4 +65,5 @@
     ],
     target_sdk_version: "33",
     manifest: "AndroidManifestWithoutProperty.xml",
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
deleted file mode 100644
index 880e826..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.cts.net;
-
-import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import android.platform.test.annotations.FlakyTest;
-
-import com.android.testutils.SkipPresubmit;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
-import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
-
-import org.junit.Test;
-
-import java.util.Map;
-
-@SkipPresubmit(reason = "Out of SLO flakiness")
-public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
-    private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
-
-    @BeforeClassWithInfo
-    public static void setUpOnce(TestInformation testInfo) throws Exception {
-        uninstallPackage(testInfo, TEST_APP2_PKG, false);
-        installPackage(testInfo, TEST_APP2_APK);
-    }
-
-    @AfterClassWithInfo
-    public static void tearDownOnce(TestInformation testInfo) throws DeviceNotAvailableException {
-        uninstallPackage(testInfo, TEST_APP2_PKG, true);
-    }
-
-    @Test
-    public void testStartActivity_batterySaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
-    }
-
-    @Test
-    public void testStartActivity_dataSaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
-    }
-
-    @FlakyTest(bugId = 231440256)
-    @Test
-    public void testStartActivity_doze() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
-    }
-
-    @Test
-    public void testStartActivity_appStandby() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
-    }
-
-    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
-    @Test
-    public void testStartActivity_default() throws Exception {
-        runDeviceTestsWithArgs(TEST_PKG, TEST_CLASS, "testStartActivity_default",
-                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
deleted file mode 100644
index 0d01fc1..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.net;
-
-import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import com.android.testutils.SkipPresubmit;
-import com.android.tradefed.device.DeviceNotAvailableException;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-// TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side tests.
-@SkipPresubmit(reason = "Monitoring for flakiness")
-public class HostsideDefaultNetworkRestrictionsTests extends HostsideNetworkTestCase {
-    private static final String METERED_TEST_CLASS = TEST_PKG + ".DefaultRestrictionsMeteredTest";
-    private static final String NON_METERED_TEST_CLASS =
-            TEST_PKG + ".DefaultRestrictionsNonMeteredTest";
-
-    @Before
-    public void setUp() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    private void runMeteredTest(String methodName) throws DeviceNotAvailableException {
-        runDeviceTestsWithArgs(TEST_PKG, METERED_TEST_CLASS, methodName,
-                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-
-    private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
-        runDeviceTestsWithArgs(TEST_PKG, NON_METERED_TEST_CLASS, methodName,
-                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-
-    @Test
-    public void testMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
-            throws Exception {
-        runMeteredTest("testActivityNetworkAccess");
-    }
-
-    @Test
-    public void testMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
-            throws Exception {
-        runMeteredTest("testFgsNetworkAccess");
-    }
-
-    @Test
-    public void testMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
-        runMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
-    }
-
-    @Test
-    public void testMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
-            throws Exception {
-        runMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
-            throws Exception {
-        runNonMeteredTest("testActivityNetworkAccess");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
-            throws Exception {
-        runNonMeteredTest("testFgsNetworkAccess");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
-        runNonMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
-            throws Exception {
-        runNonMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
-    }
-}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
deleted file mode 100644
index 361f7c7..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.cts.net;
-
-import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import com.android.testutils.SkipPresubmit;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-@SkipPresubmit(reason = "Out of SLO flakiness")
-public class HostsideNetworkCallbackTests extends HostsideNetworkTestCase {
-
-    @Before
-    public void setUp() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    @Test
-    public void testOnBlockedStatusChanged_dataSaver() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
-    }
-
-    @Test
-    public void testOnBlockedStatusChanged_powerSaver() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
-    }
-
-    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
-    @Test
-    public void testOnBlockedStatusChanged_default() throws Exception {
-        runDeviceTestsWithArgs(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
-                "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-}
-
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
deleted file mode 100644
index e97db58..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.cts.net;
-
-import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Map;
-
-public class HostsideNetworkPolicyManagerTests extends HostsideNetworkTestCase {
-    @Before
-    public void setUp() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withUidNotBlocked");
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withDataSaverMode");
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
-    }
-
-    @Test
-    public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_withPowerSaverMode");
-    }
-
-    @Test
-    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
-        runDeviceTests(TEST_PKG,
-                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
-    }
-
-    // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
-    @Test
-    public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
-        runDeviceTestsWithArgs(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
-                "testIsUidNetworkingBlocked_whenInBackground",
-                Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
-    }
-}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index ca95ed6..69d61b3 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -18,9 +18,7 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
 
-import com.android.ddmlib.Log;
 import com.android.modules.utils.build.testing.DeviceSdkLevel;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.invoker.TestInformation;
@@ -31,17 +29,11 @@
 import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
-import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
-import com.android.tradefed.util.RunUtil;
 
 import org.junit.runner.RunWith;
 
-import java.util.Map;
-
 @RunWith(DeviceJUnit4ClassRunner.class)
 abstract class HostsideNetworkTestCase extends BaseHostJUnit4Test {
-    protected static final boolean DEBUG = false;
-    protected static final String TAG = "HostsideNetworkTests";
     protected static final String TEST_PKG = "com.android.cts.net.hostside";
     protected static final String TEST_APK = "CtsHostsideNetworkTestsApp.apk";
     protected static final String TEST_APK_NEXT = "CtsHostsideNetworkTestsAppNext.apk";
@@ -103,67 +95,4 @@
             throws DeviceNotAvailableException {
         uninstallPackage(getTestInformation(), packageName, shouldSucceed);
     }
-
-    protected void assertPackageUninstalled(String packageName) throws DeviceNotAvailableException,
-            InterruptedException {
-        final String command = "cmd package list packages " + packageName;
-        final int max_tries = 5;
-        for (int i = 1; i <= max_tries; i++) {
-            final String result = runCommand(command);
-            if (result.trim().isEmpty()) {
-                return;
-            }
-            // 'list packages' filters by substring, so we need to iterate with the results
-            // and check one by one, otherwise 'com.android.cts.net.hostside' could return
-            // 'com.android.cts.net.hostside.app2'
-            boolean found = false;
-            for (String line : result.split("[\\r\\n]+")) {
-                if (line.endsWith(packageName)) {
-                    found = true;
-                    break;
-                }
-            }
-            if (!found) {
-                return;
-            }
-            i++;
-            Log.v(TAG, "Package " + packageName + " not uninstalled yet (" + result
-                    + "); sleeping 1s before polling again");
-            RunUtil.getDefault().sleep(1000);
-        }
-        fail("Package '" + packageName + "' not uinstalled after " + max_tries + " seconds");
-    }
-
-    protected int getUid(String packageName) throws DeviceNotAvailableException {
-        final int currentUser = getDevice().getCurrentUser();
-        final String uidLines = runCommand(
-                "cmd package list packages -U --user " + currentUser + " " + packageName);
-        for (String uidLine : uidLines.split("\n")) {
-            if (uidLine.startsWith("package:" + packageName + " uid:")) {
-                final String[] uidLineParts = uidLine.split(":");
-                // 3rd entry is package uid
-                return Integer.parseInt(uidLineParts[2].trim());
-            }
-        }
-        throw new IllegalStateException("Failed to find the test app on the device; pkg="
-                + packageName + ", u=" + currentUser);
-    }
-
-    protected boolean runDeviceTestsWithArgs(String packageName, String className,
-            String methodName, Map<String, String> args) throws DeviceNotAvailableException {
-        final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
-                .setTestClassName(className)
-                .setTestMethodName(methodName);
-        for (Map.Entry<String, String> arg : args.entrySet()) {
-            deviceTestRunOptions.addInstrumentationArg(arg.getKey(), arg.getValue());
-        }
-        return runDeviceTests(deviceTestRunOptions);
-    }
-
-    protected String runCommand(String command) throws DeviceNotAvailableException {
-        Log.d(TAG, "Command: '" + command + "'");
-        final String output = getDevice().executeShellCommand(command);
-        if (DEBUG) Log.v(TAG, "Output: " + output.trim());
-        return output;
-    }
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
deleted file mode 100644
index 9c3751d..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ /dev/null
@@ -1,466 +0,0 @@
-/*
- * Copyright (C) 2016 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.cts.net;
-
-import static org.junit.Assert.fail;
-
-import android.platform.test.annotations.SecurityTest;
-
-import com.android.ddmlib.Log;
-import com.android.testutils.SkipPresubmit;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.util.RunUtil;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-@SkipPresubmit(reason = "Out of SLO flakiness")
-public class HostsideRestrictBackgroundNetworkTests extends HostsideNetworkTestCase {
-
-    @Before
-    public void setUp() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, false);
-        installPackage(TEST_APP2_APK);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        uninstallPackage(TEST_APP2_PKG, true);
-    }
-
-    @SecurityTest
-    @Test
-    public void testDataWarningReceiver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
-                "testSnoozeWarningNotReceived");
-    }
-
-    /**************************
-     * Data Saver Mode tests. *
-     **************************/
-
-    @Test
-    public void testDataSaverMode_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_disabled");
-    }
-
-    @Test
-    public void testDataSaverMode_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_whitelisted");
-    }
-
-    @Test
-    public void testDataSaverMode_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_enabled");
-    }
-
-    @Test
-    public void testDataSaverMode_blacklisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_blacklisted");
-    }
-
-    @Test
-    public void testDataSaverMode_reinstall() throws Exception {
-        final int oldUid = getUid(TEST_APP2_PKG);
-
-        // Make sure whitelist is revoked when package is removed
-        addRestrictBackgroundWhitelist(oldUid);
-
-        uninstallPackage(TEST_APP2_PKG, true);
-        assertPackageUninstalled(TEST_APP2_PKG);
-        assertRestrictBackgroundWhitelist(oldUid, false);
-
-        installPackage(TEST_APP2_APK);
-        final int newUid = getUid(TEST_APP2_PKG);
-        assertRestrictBackgroundWhitelist(oldUid, false);
-        assertRestrictBackgroundWhitelist(newUid, false);
-    }
-
-    @Test
-    public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
-    }
-
-    @Test
-    public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
-                "testBroadcastNotSentOnUnsupportedDevices");
-    }
-
-    /*****************************
-     * Battery Saver Mode tests. *
-     *****************************/
-
-    @Test
-    public void testBatterySaverModeMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testBatterySaverModeMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testBatterySaverModeMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testBatterySaverMode_reinstall() throws Exception {
-        if (!isDozeModeEnabled()) {
-            Log.w(TAG, "testBatterySaverMode_reinstall() skipped because device does not support "
-                    + "Doze Mode");
-            return;
-        }
-
-        addPowerSaveModeWhitelist(TEST_APP2_PKG);
-
-        uninstallPackage(TEST_APP2_PKG, true);
-        assertPackageUninstalled(TEST_APP2_PKG);
-        assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
-
-        installPackage(TEST_APP2_APK);
-        assertPowerSaveModeWhitelist(TEST_APP2_PKG, false);
-    }
-
-    @Test
-    public void testBatterySaverModeNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testBatterySaverModeNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    /*******************
-     * App idle tests. *
-     *******************/
-
-    @Test
-    public void testAppIdleMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testAppIdleMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testAppIdleMetered_tempWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_tempWhitelisted");
-    }
-
-    @Test
-    public void testAppIdleMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testAppIdleMetered_idleWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testAppIdleNetworkAccess_idleWhitelisted");
-    }
-
-    // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
-    // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
-    //    public void testAppIdle_reinstall() throws Exception {
-    //    }
-
-    @Test
-    public void testAppIdleNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-
-    @Test
-    public void testAppIdleNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_tempWhitelisted");
-    }
-
-    @Test
-    public void testAppIdleNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testAppIdleNetworkAccess_idleWhitelisted");
-    }
-
-    @Test
-    public void testAppIdleNonMetered_whenCharging() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testAppIdleNetworkAccess_whenCharging");
-    }
-
-    @Test
-    public void testAppIdleMetered_whenCharging() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
-                "testAppIdleNetworkAccess_whenCharging");
-    }
-
-    @Test
-    public void testAppIdle_toast() throws Exception {
-        // Check that showing a toast doesn't bring an app out of standby
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
-                "testAppIdle_toast");
-    }
-
-    /********************
-     * Doze Mode tests. *
-     ********************/
-
-    @Test
-    public void testDozeModeMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testDozeModeMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testDozeModeMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
-                "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
-    }
-
-    // TODO: currently power-save mode and idle uses the same whitelist, so this test would be
-    // redundant (as it would be testing the same as testBatterySaverMode_reinstall())
-    //    public void testDozeMode_reinstall() throws Exception {
-    //    }
-
-    @Test
-    public void testDozeModeNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_disabled");
-    }
-
-    @Test
-    public void testDozeModeNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_whitelisted");
-    }
-
-    @Test
-    public void testDozeModeNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabled");
-    }
-
-    @Test
-    public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
-            throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
-                "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
-    }
-
-    /**********************
-     * Mixed modes tests. *
-     **********************/
-
-    @Test
-    public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDataAndBatterySaverModes_meteredNetwork");
-    }
-
-    @Test
-    public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDataAndBatterySaverModes_nonMeteredNetwork");
-    }
-
-    @Test
-    public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndBatterySaverMode_powerSaveWhitelists");
-    }
-
-    @Test
-    public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndAppIdle_powerSaveWhitelists");
-    }
-
-    @Test
-    public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndDoze_tempPowerSaveWhitelists");
-    }
-
-    @Test
-    public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
-    }
-
-    @Test
-    public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testDozeAndAppIdle_appIdleWhitelist");
-    }
-
-    @Test
-    public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
-    }
-
-    @Test
-    public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
-                "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
-    }
-
-    /**************************
-     * Restricted mode tests. *
-     **************************/
-
-    @Test
-    public void testNetworkAccess_restrictedMode() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
-                "testNetworkAccess");
-    }
-
-    @Test
-    public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
-                "testNetworkAccess_withBatterySaver");
-    }
-
-    /************************
-     * Expedited job tests. *
-     ************************/
-
-    @Test
-    public void testMeteredNetworkAccess_expeditedJob() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
-    }
-
-    @Test
-    public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
-    }
-
-    /*******************
-     * Helper methods. *
-     *******************/
-
-    private void assertRestrictBackgroundWhitelist(int uid, boolean expected) throws Exception {
-        final int max_tries = 5;
-        boolean actual = false;
-        for (int i = 1; i <= max_tries; i++) {
-            final String output = runCommand("cmd netpolicy list restrict-background-whitelist ");
-            actual = output.contains(Integer.toString(uid));
-            if (expected == actual) {
-                return;
-            }
-            Log.v(TAG, "whitelist check for uid " + uid + " doesn't match yet (expected "
-                    + expected + ", got " + actual + "); sleeping 1s before polling again");
-            RunUtil.getDefault().sleep(1000);
-        }
-        fail("whitelist check for uid " + uid + " failed: expected "
-                + expected + ", got " + actual);
-    }
-
-    private void assertPowerSaveModeWhitelist(String packageName, boolean expected)
-            throws Exception {
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        assertDelayedCommand("dumpsys deviceidle whitelist =" + packageName,
-                Boolean.toString(expected));
-    }
-
-    /**
-     * Asserts the result of a command, wait and re-running it a couple times if necessary.
-     */
-    private void assertDelayedCommand(String command, String expectedResult)
-            throws InterruptedException, DeviceNotAvailableException {
-        final int maxTries = 5;
-        for (int i = 1; i <= maxTries; i++) {
-            final String result = runCommand(command).trim();
-            if (result.equals(expectedResult)) return;
-            Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
-                    + expectedResult + "' on attempt #; sleeping 1s before polling again");
-            RunUtil.getDefault().sleep(1000);
-        }
-        fail("Command '" + command + "' did not return '" + expectedResult + "' after " + maxTries
-                + " attempts");
-    }
-
-    protected void addRestrictBackgroundWhitelist(int uid) throws Exception {
-        runCommand("cmd netpolicy add restrict-background-whitelist " + uid);
-        assertRestrictBackgroundWhitelist(uid, true);
-    }
-
-    private void addPowerSaveModeWhitelist(String packageName) throws Exception {
-        Log.i(TAG, "Adding package " + packageName + " to power-save-mode whitelist");
-        // TODO: currently the power-save mode is behaving like idle, but once it changes, we'll
-        // need to use netpolicy for whitelisting
-        runCommand("dumpsys deviceidle whitelist +" + packageName);
-        assertPowerSaveModeWhitelist(packageName, true);
-    }
-
-    protected boolean isDozeModeEnabled() throws Exception {
-        final String result = runCommand("cmd deviceidle enabled deep").trim();
-        return result.equals("1");
-    }
-}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index 4f21af7..cea60f9 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -18,8 +18,6 @@
 
 import android.platform.test.annotations.RequiresDevice;
 
-import com.android.testutils.SkipPresubmit;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -37,13 +35,11 @@
         uninstallPackage(TEST_APP2_PKG, true);
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testChangeUnderlyingNetworks() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testChangeUnderlyingNetworks");
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testDefault() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testDefault");
@@ -166,9 +162,20 @@
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testBlockIncomingPackets");
     }
 
-    @SkipPresubmit(reason = "Out of SLO flakiness")
     @Test
     public void testSetVpnDefaultForUids() throws Exception {
         runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetVpnDefaultForUids");
     }
+
+    @Test
+    public void testDropPacketToVpnAddress_WithoutDuplicatedAddress() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
+                "testDropPacketToVpnAddress_WithoutDuplicatedAddress");
+    }
+
+    @Test
+    public void testDropPacketToVpnAddress_WithDuplicatedAddress() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
+                "testDropPacketToVpnAddress_WithDuplicatedAddress");
+    }
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java b/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
deleted file mode 100644
index 23aca24..0000000
--- a/tests/cts/hostside/src/com/android/cts/net/NetworkPolicyTestsPreparer.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.cts.net;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.log.LogUtil;
-import com.android.tradefed.targetprep.ITargetPreparer;
-
-public class NetworkPolicyTestsPreparer implements ITargetPreparer {
-    private ITestDevice mDevice;
-    private boolean mOriginalAirplaneModeEnabled;
-    private String mOriginalAppStandbyEnabled;
-    private String mOriginalBatteryStatsConstants;
-    private final static String KEY_STABLE_CHARGING_DELAY_MS = "battery_charged_delay_ms";
-    private final static int DESIRED_STABLE_CHARGING_DELAY_MS = 0;
-
-    @Override
-    public void setUp(TestInformation testInformation) throws DeviceNotAvailableException {
-        mDevice = testInformation.getDevice();
-        mOriginalAppStandbyEnabled = getAppStandbyEnabled();
-        setAppStandbyEnabled("1");
-        LogUtil.CLog.d("Original app_standby_enabled: " + mOriginalAppStandbyEnabled);
-
-        mOriginalBatteryStatsConstants = getBatteryStatsConstants();
-        setBatteryStatsConstants(
-                KEY_STABLE_CHARGING_DELAY_MS + "=" + DESIRED_STABLE_CHARGING_DELAY_MS);
-        LogUtil.CLog.d("Original battery_saver_constants: " + mOriginalBatteryStatsConstants);
-
-        mOriginalAirplaneModeEnabled = getAirplaneModeEnabled();
-        // Turn off airplane mode in case another test left the device in that state.
-        setAirplaneModeEnabled(false);
-        LogUtil.CLog.d("Original airplane mode state: " + mOriginalAirplaneModeEnabled);
-    }
-
-    @Override
-    public void tearDown(TestInformation testInformation, Throwable e)
-            throws DeviceNotAvailableException {
-        setAirplaneModeEnabled(mOriginalAirplaneModeEnabled);
-        setAppStandbyEnabled(mOriginalAppStandbyEnabled);
-        setBatteryStatsConstants(mOriginalBatteryStatsConstants);
-    }
-
-    private void setAirplaneModeEnabled(boolean enable) throws DeviceNotAvailableException {
-        executeCmd("cmd connectivity airplane-mode " + (enable ? "enable" : "disable"));
-    }
-
-    private boolean getAirplaneModeEnabled() throws DeviceNotAvailableException {
-        return "enabled".equals(executeCmd("cmd connectivity airplane-mode").trim());
-    }
-
-    private void setAppStandbyEnabled(String appStandbyEnabled) throws DeviceNotAvailableException {
-        if ("null".equals(appStandbyEnabled)) {
-            executeCmd("settings delete global app_standby_enabled");
-        } else {
-            executeCmd("settings put global app_standby_enabled " + appStandbyEnabled);
-        }
-    }
-
-    private String getAppStandbyEnabled() throws DeviceNotAvailableException {
-        return executeCmd("settings get global app_standby_enabled").trim();
-    }
-
-    private void setBatteryStatsConstants(String batteryStatsConstants)
-            throws DeviceNotAvailableException {
-        executeCmd("settings put global battery_stats_constants \"" + batteryStatsConstants + "\"");
-    }
-
-    private String getBatteryStatsConstants() throws DeviceNotAvailableException {
-        return executeCmd("settings get global battery_stats_constants");
-    }
-
-    private String executeCmd(String cmd) throws DeviceNotAvailableException {
-        final String output = mDevice.executeShellCommand(cmd).trim();
-        LogUtil.CLog.d("Output for '%s': %s", cmd, output);
-        return output;
-    }
-}
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index 5ac4229..5f062f1 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -14,14 +14,20 @@
 
 package {
     default_applicable_licenses: ["Android-Apache-2.0"],
+    default_team: "trendy_team_fwk_core_networking",
 }
 
 python_test_host {
     name: "CtsConnectivityMultiDevicesTestCases",
-    main: "connectivity_multi_devices_test.py",
-    srcs: ["connectivity_multi_devices_test.py"],
+    main: "run_tests.py",
+    srcs: [
+        "apfv4_test.py",
+        "connectivity_multi_devices_test.py",
+        "run_tests.py",
+    ],
     libs: [
         "mobly",
+        "net-tests-utils-host-python-common",
     ],
     test_suites: [
         "cts",
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
new file mode 100644
index 0000000..4633d37
--- /dev/null
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -0,0 +1,35 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from net_tests_utils.host.python import apf_test_base
+
+# Constants.
+COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED = "DROPPED_ETHERTYPE_NOT_ALLOWED"
+ETHER_BROADCAST_ADDR = "FFFFFFFFFFFF"
+ETH_P_ETHERCAT = "88A4"
+
+
+class ApfV4Test(apf_test_base.ApfTestBase):
+
+  def test_apf_drop_ethercat(self):
+    # Ethernet header (14 bytes).
+    packet = ETHER_BROADCAST_ADDR  # Destination MAC (broadcast)
+    packet += self.server_mac_address.replace(":", "")  # Source MAC
+    packet += ETH_P_ETHERCAT  # EtherType (EtherCAT)
+
+    # EtherCAT header (2 bytes) + 44 bytes of zero padding.
+    packet += "00" * 46
+    self.send_packet_and_expect_counter_increased(
+        packet, COUNTER_DROPPED_ETHERTYPE_NOT_ALLOWED
+    )
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index ab88504..eceb535 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -1,110 +1,70 @@
-# Lint as: python3
-"""Connectivity multi devices tests."""
-import base64
-import sys
-import uuid
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
 
-from mobly import asserts
-from mobly import base_test
-from mobly import test_runner
-from mobly import utils
-from mobly.controllers import android_device
-
-CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+from net_tests_utils.host.python import mdns_utils, multi_devices_test_base, tether_utils
+from net_tests_utils.host.python.tether_utils import UpstreamType
 
 
-class UpstreamType:
-  CELLULAR = 1
-  WIFI = 2
-
-
-class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
-
-  def setup_class(self):
-    # Declare that two Android devices are needed.
-    self.clientDevice, self.serverDevice = self.register_controller(
-        android_device, min_number=2
-    )
-
-    def setup_device(device):
-      device.load_snippet(
-          "connectivity_multi_devices_snippet",
-          CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
-      )
-
-    # Set up devices in parallel to save time.
-    utils.concurrent_exec(
-        setup_device,
-        ((self.clientDevice,), (self.serverDevice,)),
-        max_workers=2,
-        raise_on_exception=True,
-    )
-
-  @staticmethod
-  def generate_uuid32_base64():
-    """Generates a UUID32 and encodes it in Base64.
-
-    Returns:
-        str: The Base64-encoded UUID32 string. Which is 22 characters.
-    """
-    return base64.b64encode(uuid.uuid1().bytes).decode("utf-8").strip("=")
-
-  def _do_test_hotspot_for_upstream_type(self, upstream_type):
-    """Test hotspot with the specified upstream type.
-
-    This test create a hotspot, make the client connect
-    to it, and verify the packet is forwarded by the hotspot.
-    """
-    server = self.serverDevice.connectivity_multi_devices_snippet
-    client = self.clientDevice.connectivity_multi_devices_snippet
-
-    # Assert pre-conditions specific to each upstream type.
-    asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
-    asserts.skip_if(
-      not server.hasHotspotFeature(), "Server requires hotspot feature"
-    )
-    if upstream_type == UpstreamType.CELLULAR:
-      asserts.skip_if(
-          not server.hasTelephonyFeature(), "Server requires Telephony feature"
-      )
-      server.requestCellularAndEnsureDefault()
-    elif upstream_type == UpstreamType.WIFI:
-      asserts.skip_if(
-          not server.isStaApConcurrencySupported(),
-          "Server requires Wifi AP + STA concurrency",
-      )
-      server.ensureWifiIsDefault()
-    else:
-      raise ValueError(f"Invalid upstream type: {upstream_type}")
-
-    # Generate ssid/passphrase with random characters to make sure nearby devices won't
-    # connect unexpectedly. Note that total length of ssid cannot go over 32.
-    testSsid = "HOTSPOT-" + self.generate_uuid32_base64()
-    testPassphrase = self.generate_uuid32_base64()
-
-    try:
-      # Create a hotspot with fixed SSID and password.
-      server.startHotspot(testSsid, testPassphrase)
-
-      # Make the client connects to the hotspot.
-      client.connectToWifi(testSsid, testPassphrase, True)
-
-    finally:
-      if upstream_type == UpstreamType.CELLULAR:
-        server.unrequestCellular()
-      # Teardown the hotspot.
-      server.stopAllTethering()
+class ConnectivityMultiDevicesTest(
+    multi_devices_test_base.MultiDevicesTestBase
+):
 
   def test_hotspot_upstream_wifi(self):
-    self._do_test_hotspot_for_upstream_type(UpstreamType.WIFI)
+    tether_utils.assume_hotspot_test_preconditions(
+        self.serverDevice, self.clientDevice, UpstreamType.WIFI
+    )
+    try:
+      # Connectivity of the client verified by asserting the validated capability.
+      tether_utils.setup_hotspot_and_client_for_upstream_type(
+          self.serverDevice, self.clientDevice, UpstreamType.WIFI
+      )
+    finally:
+      tether_utils.cleanup_tethering_for_upstream_type(
+          self.serverDevice, UpstreamType.WIFI
+      )
 
   def test_hotspot_upstream_cellular(self):
-    self._do_test_hotspot_for_upstream_type(UpstreamType.CELLULAR)
+    tether_utils.assume_hotspot_test_preconditions(
+        self.serverDevice, self.clientDevice, UpstreamType.CELLULAR
+    )
+    try:
+      # Connectivity of the client verified by asserting the validated capability.
+      tether_utils.setup_hotspot_and_client_for_upstream_type(
+          self.serverDevice, self.clientDevice, UpstreamType.CELLULAR
+      )
+    finally:
+      tether_utils.cleanup_tethering_for_upstream_type(
+          self.serverDevice, UpstreamType.CELLULAR
+      )
 
-
-if __name__ == "__main__":
-  # Take test args
-  if "--" in sys.argv:
-    index = sys.argv.index("--")
-    sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
-  test_runner.main()
+  def test_mdns_via_hotspot(self):
+    tether_utils.assume_hotspot_test_preconditions(
+        self.serverDevice, self.clientDevice, UpstreamType.NONE
+    )
+    mdns_utils.assume_mdns_test_preconditions(
+        self.clientDevice, self.serverDevice
+    )
+    try:
+      # Connectivity of the client verified by asserting the validated capability.
+      tether_utils.setup_hotspot_and_client_for_upstream_type(
+          self.serverDevice, self.clientDevice, UpstreamType.NONE
+      )
+      mdns_utils.register_mdns_service_and_discover_resolve(
+          self.clientDevice, self.serverDevice
+      )
+    finally:
+      mdns_utils.cleanup_mdns_service(self.clientDevice, self.serverDevice)
+      tether_utils.cleanup_tethering_for_upstream_type(
+          self.serverDevice, UpstreamType.NONE
+      )
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
new file mode 100644
index 0000000..1391d13
--- /dev/null
+++ b/tests/cts/multidevices/run_tests.py
@@ -0,0 +1,38 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+"""Main entrypoint for all of test cases."""
+
+import sys
+from apfv4_test import ApfV4Test
+from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
+from mobly import suite_runner
+
+
+if __name__ == "__main__":
+  # For MoblyBinaryHostTest, this entry point will be called twice:
+  # 1. List tests.
+  #   <mobly-par-file-name> -- --list_tests
+  # 2. Run tests.
+  #   <mobly-par-file-name> -- --config=<yaml-path> \
+  #      --device_serial=<device-serial> --log_path=<log-path>
+  # Strip the "--" since suite runner doesn't recognize it.
+  # While the parameters before "--" is for the infrastructure,
+  # ignore them if any. Also, do not alter parameters if there
+  # is no "--", in case the binary is invoked manually.
+  if "--" in sys.argv:
+    index = sys.argv.index("--")
+    sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
+  # TODO: make the tests can be executed without manually list classes.
+  suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test], sys.argv)
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
index 5940cbb..c94087e 100644
--- a/tests/cts/multidevices/snippet/Android.bp
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -25,6 +25,8 @@
     ],
     srcs: [
         "ConnectivityMultiDevicesSnippet.kt",
+        "MdnsMultiDevicesSnippet.kt",
+        "Wifip2pMultiDevicesSnippet.kt",
     ],
     manifest: "AndroidManifest.xml",
     static_libs: [
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
index 9ed8146..4637497 100644
--- a/tests/cts/multidevices/snippet/AndroidManifest.xml
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -21,13 +21,17 @@
   <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
   <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
   <uses-permission android:name="android.permission.INTERNET" />
+  <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
+                   android:usesPermissionFlags="neverForLocation" />
   <application>
     <!-- Add any classes that implement the Snippet interface as meta-data, whose
          value is a comma-separated string, each section being the package path
          of a snippet class -->
     <meta-data
         android:name="mobly-snippets"
-        android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet" />
+        android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet,
+                       com.google.snippet.connectivity.MdnsMultiDevicesSnippet,
+                       com.google.snippet.connectivity.Wifip2pMultiDevicesSnippet" />
   </application>
   <!-- Add an instrumentation tag so that the app can be launched through an
        instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 115210b..7368669 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -16,6 +16,7 @@
 
 package com.google.snippet.connectivity
 
+import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.OVERRIDE_WIFI_CONFIG
 import android.content.pm.PackageManager.FEATURE_TELEPHONY
 import android.content.pm.PackageManager.FEATURE_WIFI
@@ -30,27 +31,38 @@
 import android.net.wifi.SoftApConfiguration
 import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
 import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiNetworkSpecifier
 import android.net.wifi.WifiSsid
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.AutoReleaseNetworkCallbackRule
 import com.android.testutils.ConnectUtil
-import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.NetworkCallbackHelper
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.runAsShell
 import com.google.android.mobly.snippet.Snippet
 import com.google.android.mobly.snippet.rpc.Rpc
+import org.junit.Rule
 
 class ConnectivityMultiDevicesSnippet : Snippet {
+    @get:Rule
+    val networkCallbackRule = AutoReleaseNetworkCallbackRule()
     private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
     private val wifiManager = context.getSystemService(WifiManager::class.java)!!
     private val cm = context.getSystemService(ConnectivityManager::class.java)!!
     private val pm = context.packageManager
     private val ctsNetUtils = CtsNetUtils(context)
+    private val cbHelper = NetworkCallbackHelper()
     private val ctsTetheringUtils = CtsTetheringUtils(context)
     private var oldSoftApConfig: SoftApConfiguration? = null
 
+    override fun shutdown() {
+        cbHelper.unregisterAll()
+    }
+
     @Rpc(description = "Check whether the device has wifi feature.")
     fun hasWifiFeature() = pm.hasSystemFeature(FEATURE_WIFI)
 
@@ -58,20 +70,21 @@
     fun hasTelephonyFeature() = pm.hasSystemFeature(FEATURE_TELEPHONY)
 
     @Rpc(description = "Check whether the device supporters AP + STA concurrency.")
-    fun isStaApConcurrencySupported() {
-        wifiManager.isStaApConcurrencySupported()
-    }
+    fun isStaApConcurrencySupported() = wifiManager.isStaApConcurrencySupported()
+
+    @Rpc(description = "Check whether the device SDK is as least T")
+    fun isAtLeastT() = SdkLevel.isAtLeastT()
 
     @Rpc(description = "Request cellular connection and ensure it is the default network.")
     fun requestCellularAndEnsureDefault() {
         ctsNetUtils.disableWifi()
-        val network = ctsNetUtils.connectToCell()
+        val network = cbHelper.requestCell()
         ctsNetUtils.expectNetworkIsSystemDefault(network)
     }
 
-    @Rpc(description = "Unrequest cellular connection.")
-    fun unrequestCellular() {
-        ctsNetUtils.disconnectFromCell()
+    @Rpc(description = "Unregister all connections.")
+    fun unregisterAll() {
+        cbHelper.unregisterAll()
     }
 
     @Rpc(description = "Ensure any wifi is connected and is the default network.")
@@ -84,10 +97,8 @@
     // Suppress warning because WifiManager methods to connect to a config are
     // documented not to be deprecated for privileged users.
     @Suppress("DEPRECATION")
-    fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Network {
+    fun connectToWifi(ssid: String, passphrase: String): Long {
         val specifier = WifiNetworkSpecifier.Builder()
-            .setSsid(ssid)
-            .setWpa2Passphrase(passphrase)
             .setBand(ScanResult.WIFI_BAND_24_GHZ)
             .build()
         val wifiConfig = WifiConfiguration()
@@ -98,30 +109,37 @@
         wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
         wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
 
-        // Register network callback for the specific wifi.
+        // Add the test configuration and connect to it.
+        val connectUtil = ConnectUtil(context)
+        connectUtil.connectToWifiConfig(wifiConfig)
+
+        // Implement manual SSID matching. Specifying the SSID in
+        // NetworkSpecifier is ineffective
+        // (see WifiNetworkAgentSpecifier#canBeSatisfiedBy for details).
+        // Note that holding permission is necessary when waiting for
+        // the callbacks. The handler thread checks permission; if
+        // it's not present, the SSID will be redacted.
         val networkCallback = TestableNetworkCallback()
-        val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI)
-            .setNetworkSpecifier(specifier)
-            .build()
-        cm.registerNetworkCallback(wifiRequest, networkCallback)
-
-        try {
-            // Add the test configuration and connect to it.
-            val connectUtil = ConnectUtil(context)
-            connectUtil.connectToWifiConfig(wifiConfig)
-
-            val event = networkCallback.expect<Available>()
-            if (requireValidation) {
-                networkCallback.eventuallyExpect<CapabilitiesChanged> {
-                    it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
-                }
-            }
-            return event.network
-        } finally {
-            cm.unregisterNetworkCallback(networkCallback)
+        val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build()
+        return runAsShell(NETWORK_SETTINGS) {
+            // Register the network callback is needed here.
+            // This is to avoid the race condition where callback is fired before
+            // acquiring permission.
+            networkCallbackRule.registerNetworkCallback(wifiRequest, networkCallback)
+            return@runAsShell networkCallback.eventuallyExpect<CapabilitiesChanged> {
+                // Remove double quotes.
+                val ssidFromCaps = (WifiInfo::sanitizeSsid)(it.caps.ssid)
+                ssidFromCaps == ssid && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+            }.network.networkHandle
         }
     }
 
+    @Rpc(description = "Get interface name from NetworkHandle")
+    fun getInterfaceNameFromNetworkHandle(networkHandle: Long): String {
+        val network = Network.fromNetworkHandle(networkHandle)
+        return cm.getLinkProperties(network)!!.getInterfaceName()!!
+    }
+
     @Rpc(description = "Check whether the device supports hotspot feature.")
     fun hasHotspotFeature(): Boolean {
         val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
@@ -133,7 +151,7 @@
     }
 
     @Rpc(description = "Start a hotspot with given SSID and passphrase.")
-    fun startHotspot(ssid: String, passphrase: String) {
+    fun startHotspot(ssid: String, passphrase: String): String {
         // Store old config.
         runAsShell(OVERRIDE_WIFI_CONFIG) {
             oldSoftApConfig = wifiManager.getSoftApConfiguration()
@@ -150,7 +168,7 @@
         val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
         try {
             tetheringCallback.expectNoTetheringActive()
-            ctsTetheringUtils.startWifiTethering(tetheringCallback)
+            return ctsTetheringUtils.startWifiTethering(tetheringCallback).getInterface()
         } finally {
             ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
         }
diff --git a/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
new file mode 100644
index 0000000..1b288df
--- /dev/null
+++ b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.snippet.connectivity
+
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import com.android.testutils.NsdRegistrationRecord
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import com.android.testutils.NsdResolveRecord
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ServiceResolved
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+import kotlin.test.assertEquals
+import org.junit.Assert.assertArrayEquals
+
+private const val SERVICE_NAME = "MultiDevicesTest"
+private const val SERVICE_TYPE = "_multi_devices._tcp"
+private const val SERVICE_ATTRIBUTES_KEY = "key"
+private const val SERVICE_ATTRIBUTES_VALUE = "value"
+private const val SERVICE_PORT = 12345
+private const val REGISTRATION_TIMEOUT_MS = 10_000L
+
+class MdnsMultiDevicesSnippet : Snippet {
+    private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+    private val nsdManager = context.getSystemService(NsdManager::class.java)!!
+    private val registrationRecord = NsdRegistrationRecord()
+    private val discoveryRecord = NsdDiscoveryRecord()
+    private val resolveRecord = NsdResolveRecord()
+
+    @Rpc(description = "Register a mDns service")
+    fun registerMDnsService() {
+        val info = NsdServiceInfo()
+        info.setServiceName(SERVICE_NAME)
+        info.setServiceType(SERVICE_TYPE)
+        info.setPort(SERVICE_PORT)
+        info.setAttribute(SERVICE_ATTRIBUTES_KEY, SERVICE_ATTRIBUTES_VALUE)
+        nsdManager.registerService(info, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+        registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+    }
+
+    @Rpc(description = "Unregister a mDns service")
+    fun unregisterMDnsService() {
+        nsdManager.unregisterService(registrationRecord)
+        registrationRecord.expectCallback<ServiceUnregistered>()
+    }
+
+    @Rpc(description = "Ensure the discovery and resolution of the mDNS service")
+    // Suppress the warning, as the NsdManager#resolveService() method is deprecated.
+    @Suppress("DEPRECATION")
+    fun ensureMDnsServiceDiscoveryAndResolution() {
+        // Discover a mDns service that matches the test service
+        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+        val info = discoveryRecord.waitForServiceDiscovered(SERVICE_NAME, SERVICE_TYPE)
+        // Resolve the retrieved mDns service.
+        nsdManager.resolveService(info, resolveRecord)
+        val serviceResolved = resolveRecord.expectCallbackEventually<ServiceResolved>()
+        serviceResolved.serviceInfo.let {
+            assertEquals(SERVICE_NAME, it.serviceName)
+            assertEquals(".$SERVICE_TYPE", it.serviceType)
+            assertEquals(SERVICE_PORT, it.port)
+            assertEquals(1, it.attributes.size)
+            assertArrayEquals(
+                    SERVICE_ATTRIBUTES_VALUE.encodeToByteArray(),
+                    it.attributes[SERVICE_ATTRIBUTES_KEY]
+            )
+        }
+    }
+
+    @Rpc(description = "Stop discovery")
+    fun stopMDnsServiceDiscovery() {
+        nsdManager.stopServiceDiscovery(discoveryRecord)
+        discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+    }
+}
diff --git a/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
new file mode 100644
index 0000000..e0929bb
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Wifip2pMultiDevicesSnippet.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.snippet.connectivity
+
+import android.net.wifi.WifiManager
+import android.net.wifi.p2p.WifiP2pManager
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.fail
+
+private const val TIMEOUT_MS = 60000L
+
+class Wifip2pMultiDevicesSnippet : Snippet {
+    private val context by lazy { InstrumentationRegistry.getInstrumentation().getTargetContext() }
+    private val wifiManager by lazy {
+        context.getSystemService(WifiManager::class.java)
+                ?: fail("Could not get WifiManager service")
+    }
+    private val wifip2pManager by lazy {
+        context.getSystemService(WifiP2pManager::class.java)
+                ?: fail("Could not get WifiP2pManager service")
+    }
+    private lateinit var wifip2pChannel: WifiP2pManager.Channel
+
+    @Rpc(description = "Check whether the device supports Wi-Fi P2P.")
+    fun isP2pSupported() = wifiManager.isP2pSupported()
+
+    @Rpc(description = "Start Wi-Fi P2P")
+    fun startWifiP2p() {
+        // Initialize Wi-Fi P2P
+        wifip2pChannel = wifip2pManager.initialize(context, context.mainLooper, null)
+
+        // Ensure the Wi-Fi P2P channel is available
+        val p2pStateEnabledFuture = CompletableFuture<Boolean>()
+        wifip2pManager.requestP2pState(wifip2pChannel) { state ->
+            if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
+                p2pStateEnabledFuture.complete(true)
+            }
+        }
+        p2pStateEnabledFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+    }
+
+    @Rpc(description = "Stop Wi-Fi P2P")
+    fun stopWifiP2p() {
+        if (this::wifip2pChannel.isInitialized) {
+            wifip2pManager.cancelConnect(wifip2pChannel, null)
+            wifip2pManager.removeGroup(wifip2pChannel, null)
+        }
+    }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 074c587..1cd8327 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -46,6 +46,7 @@
     ],
     jarjar_rules: "jarjar-rules-shared.txt",
     static_libs: [
+        "ApfGeneratorLib",
         "bouncycastle-unbundled",
         "FrameworksNetCommonTests",
         "core-tests-support",
@@ -110,25 +111,6 @@
     min_sdk_version: "30",
 }
 
-// Networking CTS tests that target the latest released SDK. These tests can be installed on release
-// devices at any point in the Android release cycle and are useful for qualifying mainline modules
-// on release devices.
-android_test {
-    name: "CtsNetTestCasesLatestSdk",
-    defaults: [
-        "ConnectivityTestsLatestSdkDefaults",
-        "CtsNetTestCasesDefaults",
-        "CtsNetTestCasesApiStableDefaults",
-    ],
-    test_suites: [
-        "general-tests",
-        "mts-dnsresolver",
-        "mts-networking",
-        "mts-tethering",
-        "mts-wifi",
-    ],
-}
-
 java_defaults {
     name: "CtsNetTestCasesMaxTargetSdkDefaults",
     defaults: [
@@ -179,4 +161,5 @@
         "cts",
         "general-tests",
     ],
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 38f26d8..24431a6 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -24,6 +24,12 @@
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk" />
     <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
     <option name="not-shardable" value="true" />
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DynamicConfigPusher">
+        <option name="has-server-side-config" value="false" />
+        <option name="target" value="device" />
+        <option name="config-filename" value="{MODULE}" />
+        <option name="version" value="1.0" />
+    </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="{MODULE}.apk" />
@@ -38,12 +44,13 @@
         <option name="runtime-hint" value="9m4s" />
         <option name="hidden-api-checks" value="false" />
         <option name="isolated-storage" value="false" />
+        <option name="instrumentation-arg" key="test-module-name" value="{MODULE}" />
         <!-- Test filter that allows test APKs to select which tests they want to run by annotating
              those tests with an annotation matching the name of the APK.
 
              This allows us to maintain one AndroidTestTemplate.xml for all CtsNetTestCases*.apk,
-             and have CtsNetTestCases and CtsNetTestCasesLatestSdk run all tests, but have
-             CtsNetTestCasesMaxTargetSdk31 run only tests that require target SDK 31.
+             and have CtsNetTestCases run all tests, but have CtsNetTestCasesMaxTargetSdk31 run only
+             tests that require target SDK 31.
 
              This relies on the fact that if the class specified in include-annotation exists, then
              the runner will only run the tests annotated with that annotation, but if it does not,
diff --git a/Cronet/tests/cts/assets/html/hello_world.html b/tests/cts/net/DynamicConfig.xml
similarity index 65%
rename from Cronet/tests/cts/assets/html/hello_world.html
rename to tests/cts/net/DynamicConfig.xml
index ea62ce2..af019c2 100644
--- a/Cronet/tests/cts/assets/html/hello_world.html
+++ b/tests/cts/net/DynamicConfig.xml
@@ -1,5 +1,5 @@
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ Copyright (C) 2024 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
   ~ limitations under the License.
   -->
 
-<html>
-<head>
-    <title>hello world</title>
-</head>
-<body>
-<h3>hello world</h3><br>
-</body>
-</html>
\ No newline at end of file
+<dynamicConfig>
+    <entry key="remote_config_required">
+        <value>false</value>
+    </entry>
+    <entry key="IP_ADDRESS_ECHO_URL">
+        <value>https://google-ipv6test.appspot.com/ip.js?fmt=text</value>
+    </entry>
+</dynamicConfig>
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 2ec3a70..587d5a5 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -56,4 +56,5 @@
         ":CtsNetTestAppForApi23",
     ],
     per_testcase_directory: true,
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index 8e24fba..de4a3bf 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -3,6 +3,11 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+filegroup {
+    name: "dns_async_test_default_map",
+    srcs: ["dns_async_test_default.map"],
+}
+
 cc_defaults {
     name: "dns_async_defaults",
 
@@ -20,6 +25,14 @@
     srcs: [
         "NativeDnsAsyncTest.cpp",
     ],
+    // This test runs on older platform versions, so many libraries (such as libbase and libc++)
+    // need to be linked statically. The test also needs to be linked with a version script to
+    // ensure that the statically-linked library isn't exported from the executable, where it
+    // would override the shared libraries that the platform itself uses.
+    // See http://b/333438055 for an example of what goes wrong when libc++ is partially exported
+    // from an executable.
+    version_script: ":dns_async_test_default_map",
+    stl: "libc++_static",
     shared_libs: [
         "libandroid",
         "liblog",
diff --git a/tests/cts/net/native/dns/dns_async_test_default.map b/tests/cts/net/native/dns/dns_async_test_default.map
new file mode 100644
index 0000000..e342e43
--- /dev/null
+++ b/tests/cts/net/native/dns/dns_async_test_default.map
@@ -0,0 +1,20 @@
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+{
+  local:
+    *;
+};
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 5c02b0d..e2fdd3d 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -27,35 +27,32 @@
 
 using namespace android::bpf;
 
-void doBpfStructSizeTest(const char *elfPath) {
+void doBpfStructSizeTest(const char *elfPath, unsigned mapSz, unsigned progSz) {
   std::ifstream elfFile(elfPath, std::ios::in | std::ios::binary);
   ASSERT_TRUE(elfFile.is_open());
 
-  if (android::modules::sdklevel::IsAtLeastU()) {
-    EXPECT_EQ(120, readSectionUint("size_of_bpf_map_def", elfFile, 0));
-    EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
-  } else if (android::modules::sdklevel::IsAtLeastT()) {
-    EXPECT_EQ(116, readSectionUint("size_of_bpf_map_def", elfFile, 0));
-    EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
-  } else {
-    EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
-    EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
-  }
-}
-
-TEST(BpfTest, bpfStructSizeTestPreT) {
-  if (android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() << "T+ device.";
-  doBpfStructSizeTest("/system/etc/bpf/netd.o");
-  doBpfStructSizeTest("/system/etc/bpf/clatd.o");
+  EXPECT_EQ(mapSz, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+  EXPECT_EQ(progSz, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
 }
 
 TEST(BpfTest, bpfStructSizeTest) {
-  if (android::modules::sdklevel::IsAtLeastU()) {
-      doBpfStructSizeTest("/system/etc/bpf/gpuMem.o");
-      doBpfStructSizeTest("/system/etc/bpf/timeInState.o");
+  if (android::modules::sdklevel::IsAtLeastV()) {
+    // Due to V+ using mainline netbpfload, there is no longer a need to
+    // enforce consistency between platform and mainline bpf .o files.
+    GTEST_SKIP() << "V+ device.";
+  } else if (android::modules::sdklevel::IsAtLeastU()) {
+    doBpfStructSizeTest("/system/etc/bpf/gpuMem.o", 120, 92);
+    doBpfStructSizeTest("/system/etc/bpf/timeInState.o", 120, 92);
+  } else if (android::modules::sdklevel::IsAtLeastT()) {
+    doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o", 116, 92);
+    doBpfStructSizeTest("/system/etc/bpf/time_in_state.o", 116, 92);
+  } else if (android::modules::sdklevel::IsAtLeastS()) {
+    // These files were moved to mainline in Android T
+    doBpfStructSizeTest("/system/etc/bpf/netd.o", 48, 28);
+    doBpfStructSizeTest("/system/etc/bpf/clatd.o", 48, 28);
   } else {
-      doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o");
-      doBpfStructSizeTest("/system/etc/bpf/time_in_state.o");
+    // There is no mainline bpf code before S.
+    GTEST_SKIP() << "R- device.";
   }
 }
 
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
new file mode 100644
index 0000000..9ac2c67
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -0,0 +1,733 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// ktlint does not allow annotating function argument literals inline. Disable the specific rule
+// since this negatively affects readability.
+@file:Suppress("ktlint:standard:comment-wrapping")
+
+package android.net.cts
+
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.apf.ApfCapabilities
+import android.net.apf.ApfConstants.ETH_ETHERTYPE_OFFSET
+import android.net.apf.ApfConstants.ETH_HEADER_LEN
+import android.net.apf.ApfConstants.ICMP6_CHECKSUM_OFFSET
+import android.net.apf.ApfConstants.ICMP6_TYPE_OFFSET
+import android.net.apf.ApfConstants.IPV6_DEST_ADDR_OFFSET
+import android.net.apf.ApfConstants.IPV6_HEADER_LEN
+import android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET
+import android.net.apf.ApfConstants.IPV6_SRC_ADDR_OFFSET
+import android.net.apf.ApfCounterTracker
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_PING
+import android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
+import android.net.apf.ApfV4Generator
+import android.net.apf.ApfV4GeneratorBase
+import android.net.apf.ApfV6Generator
+import android.net.apf.BaseApfGenerator
+import android.net.apf.BaseApfGenerator.MemorySlot
+import android.net.apf.BaseApfGenerator.Register.R0
+import android.net.apf.BaseApfGenerator.Register.R1
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.PowerManager
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.AF_INET6
+import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.IPPROTO_ICMPV6
+import android.system.OsConstants.SOCK_DGRAM
+import android.system.OsConstants.SOCK_NONBLOCK
+import android.util.Log
+import androidx.test.filters.RequiresDevice
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel
+import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
+import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.compatibility.common.util.VsrTest
+import com.android.internal.util.HexDump
+import com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN
+import com.android.net.module.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_HEADER_MIN_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
+import com.android.net.module.util.PacketReader
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.NetworkStackModuleTest
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.SkipPresubmit
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import com.google.common.truth.Expect
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.truth.TruthJUnit.assume
+import java.io.FileDescriptor
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import kotlin.random.Random
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG = "ApfIntegrationTest"
+private const val TIMEOUT_MS = 2000L
+private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
+private const val POLLING_INTERVAL_MS: Int = 100
+private const val RCV_BUFFER_SIZE = 1480
+private const val PING_HEADER_LENGTH = 8
+
+@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+@RunWith(DevSdkIgnoreRunner::class)
+@RequiresDevice
+@NetworkStackModuleTest
+// ByteArray.toHexString is experimental API
+@kotlin.ExperimentalStdlibApi
+class ApfIntegrationTest {
+    companion object {
+        private val PING_DESTINATION = InetSocketAddress("2001:4860:4860::8888", 0)
+
+        private val context = InstrumentationRegistry.getInstrumentation().context
+        private val powerManager = context.getSystemService(PowerManager::class.java)!!
+        private val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
+
+        fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
+            var polling_time = 0
+            do {
+                Thread.sleep(POLLING_INTERVAL_MS.toLong())
+                polling_time += POLLING_INTERVAL_MS
+                if (condition()) return true
+            } while (polling_time < timeout_ms)
+            return false
+        }
+
+        fun turnScreenOff() {
+            if (!wakeLock.isHeld()) wakeLock.acquire()
+            runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
+            val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
+            assertThat(result).isTrue()
+        }
+
+        fun turnScreenOn() {
+            if (wakeLock.isHeld()) wakeLock.release()
+            runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
+            val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
+            assertThat(result).isTrue()
+        }
+
+        @BeforeClass
+        @JvmStatic
+        @Suppress("ktlint:standard:no-multi-spaces")
+        fun setupOnce() {
+            // TODO: assertions thrown in @BeforeClass / @AfterClass are not well supported in the
+            // test infrastructure. Consider saving excepion and throwing it in setUp().
+            // APF must run when the screen is off and the device is not interactive.
+            turnScreenOff()
+            // Wait for APF to become active.
+            Thread.sleep(1000)
+            // TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
+            // created.
+            // APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
+            // LegacyApfFilter.java from being used.
+            runAsShell(WRITE_DEVICE_CONFIG) {
+                DeviceConfig.setProperty(
+                        NAMESPACE_CONNECTIVITY,
+                        APF_NEW_RA_FILTER_VERSION,
+                        "1",  // value => force enabled
+                        false // makeDefault
+                )
+            }
+        }
+
+        @AfterClass
+        @JvmStatic
+        fun tearDownOnce() {
+            turnScreenOn()
+        }
+    }
+
+    class Icmp6PacketReader(
+            handler: Handler,
+            private val network: Network
+    ) : PacketReader(handler, RCV_BUFFER_SIZE) {
+        private var sockFd: FileDescriptor? = null
+        private var futureReply: CompletableFuture<ByteArray>? = null
+
+        override fun createFd(): FileDescriptor {
+            // sockFd is closed by calling super.stop()
+            val sock = Os.socket(AF_INET6, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_ICMPV6)
+            // APF runs only on WiFi, so make sure the socket is bound to the right network.
+            network.bindSocket(sock)
+            sockFd = sock
+            return sock
+        }
+
+        override fun handlePacket(recvbuf: ByteArray, length: Int) {
+            // If zero-length or Type is not echo reply: ignore.
+            if (length == 0 || recvbuf[0] != 0x81.toByte()) {
+                return
+            }
+            // Only copy the ping data and complete the future.
+            val result = recvbuf.sliceArray(8..<length)
+            Log.i(TAG, "Received ping reply: ${result.toHexString()}")
+            futureReply!!.complete(recvbuf.sliceArray(8..<length))
+        }
+
+        fun sendPing(data: ByteArray, payloadSize: Int) {
+            require(data.size == payloadSize)
+
+            // rfc4443#section-4.1: Echo Request Message
+            //   0                   1                   2                   3
+            //   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //  |     Type      |     Code      |          Checksum             |
+            //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //  |           Identifier          |        Sequence Number        |
+            //  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //  |     Data ...
+            //  +-+-+-+-+-
+            val icmp6Header = byteArrayOf(0x80.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
+            val packet = icmp6Header + data
+            Log.i(TAG, "Sent ping: ${packet.toHexString()}")
+            futureReply = CompletableFuture<ByteArray>()
+            Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
+        }
+
+        fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): ByteArray {
+            return futureReply!!.get(timeoutMs, TimeUnit.MILLISECONDS)
+        }
+
+        fun expectPingDropped() {
+            assertFailsWith(TimeoutException::class) {
+                futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+            }
+        }
+
+        override fun start(): Boolean {
+            // Ignore the fact start() could return false or throw an exception.
+            handler.post({ super.start() })
+            handler.waitForIdle(TIMEOUT_MS)
+            return true
+        }
+
+        override fun stop() {
+            handler.post({ super.stop() })
+            handler.waitForIdle(TIMEOUT_MS)
+        }
+    }
+
+    @get:Rule val ignoreRule = DevSdkIgnoreRule()
+    @get:Rule val expect = Expect.create()
+
+    private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
+    private val pm by lazy { context.packageManager }
+    private lateinit var network: Network
+    private lateinit var ifname: String
+    private lateinit var networkCallback: TestableNetworkCallback
+    private lateinit var caps: ApfCapabilities
+    private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+    private lateinit var packetReader: Icmp6PacketReader
+
+    fun getApfCapabilities(): ApfCapabilities {
+        val caps = runShellCommand("cmd network_stack apf $ifname capabilities").trim()
+        if (caps.isEmpty()) {
+            return ApfCapabilities(0, 0, 0)
+        }
+        val (version, maxLen, packetFormat) = caps.split(",").map { it.toInt() }
+        return ApfCapabilities(version, maxLen, packetFormat)
+    }
+
+    @Before
+    fun setUp() {
+        assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
+
+        networkCallback = TestableNetworkCallback()
+        cm.requestNetwork(
+                NetworkRequest.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                        .build(),
+                networkCallback
+        )
+        network = networkCallback.expect<Available>().network
+        networkCallback.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
+            ifname = assertNotNull(it.lp.interfaceName)
+            true
+        }
+        // It's possible the device does not support APF, in which case this command will not be
+        // successful. Ignore the error as testApfCapabilities() already asserts APF support on the
+        // respective VSR releases and all other tests are based on the capabilities indicated.
+        runShellCommand("cmd network_stack apf $ifname pause")
+        caps = getApfCapabilities()
+
+        packetReader = Icmp6PacketReader(handler, network)
+        packetReader.start()
+    }
+
+    @After
+    fun tearDown() {
+        if (::packetReader.isInitialized) {
+            packetReader.stop()
+        }
+        handlerThread.quitSafely()
+        handlerThread.join()
+
+        if (::ifname.isInitialized) {
+            runShellCommand("cmd network_stack apf $ifname resume")
+        }
+        if (::networkCallback.isInitialized) {
+            cm.unregisterNetworkCallback(networkCallback)
+        }
+    }
+
+    @VsrTest(
+        requirements = ["VSR-5.3.12-001", "VSR-5.3.12-003", "VSR-5.3.12-004", "VSR-5.3.12-009",
+            "VSR-5.3.12-012"]
+    )
+    @Test
+    fun testApfCapabilities() {
+        // APF became mandatory in Android 14 VSR.
+        assume().that(getVsrApiLevel()).isAtLeast(34)
+
+        // ApfFilter does not support anything but ARPHRD_ETHER.
+        assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+
+        // DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
+        // - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
+        //   the getApfPacketFilterCapabilities HAL method.
+        // - [GMS-VSR-5.3.12-004] MUST indicate at least 1024 bytes of usable memory from calls to
+        //   the getApfPacketFilterCapabilities HAL method.
+        // TODO: check whether above text should be changed "34 or higher"
+        assertThat(caps.apfVersionSupported).isAtLeast(4)
+        assertThat(caps.maximumApfProgramSize).isAtLeast(1024)
+
+        if (caps.apfVersionSupported > 4) {
+            assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
+            assertThat(caps.apfVersionSupported).isEqualTo(6000) // v6.0000
+        }
+
+        // DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
+        // ro.board.first_api_level or ro.board.api_level to 202404 or higher:
+        // - [GMS-VSR-5.3.12-009] MUST indicate at least 2048 bytes of usable memory from calls to
+        //   the getApfPacketFilterCapabilities HAL method.
+        if (getVsrApiLevel() >= 202404) {
+            assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
+        }
+    }
+
+    // APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
+    fun assumeApfVersionSupportAtLeast(version: Int) {
+        assume().that(caps.apfVersionSupported).isAtLeast(version)
+    }
+
+    fun installProgram(bytes: ByteArray) {
+        val prog = bytes.toHexString()
+        val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
+        // runShellCommandOrThrow only throws on S+.
+        assertThat(result).isEqualTo("success")
+    }
+
+    fun readProgram(): ByteArray {
+        val progHexString = runShellCommandOrThrow("cmd network_stack apf $ifname read").trim()
+        // runShellCommandOrThrow only throws on S+.
+        assertThat(progHexString).isNotEmpty()
+        return HexDump.hexStringToByteArray(progHexString)
+    }
+
+    @VsrTest(
+            requirements = ["VSR-5.3.12-007", "VSR-5.3.12-008", "VSR-5.3.12-010", "VSR-5.3.12-011"]
+    )
+    @SkipPresubmit(reason = "This test takes longer than 1 minute, do not run it on presubmit.")
+    // APF integration is mostly broken before V, only run the full read / write test on V+.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    // Increase timeout for test to 15 minutes to accommodate device with large APF RAM.
+    @Test(timeout = 15 * 60 * 1000)
+    fun testReadWriteProgram() {
+        assumeApfVersionSupportAtLeast(4)
+
+        val minReadWriteSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            2
+        } else {
+            8
+        }
+
+        // The minReadWriteSize is 2 bytes. The first byte always stays PASS.
+        val program = ByteArray(caps.maximumApfProgramSize)
+        for (i in caps.maximumApfProgramSize downTo minReadWriteSize) {
+            // Randomize bytes in range [1, i). And install first [0, i) bytes of program.
+            // Note that only the very first instruction (PASS) is valid APF bytecode.
+            Random.nextBytes(program, 1 /* fromIndex */, i /* toIndex */)
+            installProgram(program.sliceArray(0..<i))
+
+            // Compare entire memory region.
+            val readResult = readProgram()
+            val errMsg = """
+                read/write $i byte prog failed.
+                In APFv4, the APF memory region MUST NOT be modified or cleared except by APF
+                instructions executed by the interpreter or by Android OS calls to the HAL. If this
+                requirement cannot be met, the firmware cannot declare that it supports APFv4 and
+                it should declare that it only supports APFv3(if counter is partially supported) or
+                APFv2.
+            """.trimIndent()
+            assertWithMessage(errMsg).that(readResult).isEqualTo(program)
+        }
+    }
+
+    private fun installAndVerifyProgram(program: ByteArray) {
+        installProgram(program)
+        val readResult = readProgram().take(program.size).toByteArray()
+        assertThat(readResult).isEqualTo(program)
+    }
+
+    fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
+        // If not IPv6 -> PASS
+        addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+        addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), BaseApfGenerator.PASS_LABEL)
+
+        // If not ICMPv6 -> PASS
+        addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+        addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), BaseApfGenerator.PASS_LABEL)
+
+        // If not echo reply -> PASS
+        addLoad8(R0, ICMP6_TYPE_OFFSET)
+        addJumpIfR0NotEquals(0x81, BaseApfGenerator.PASS_LABEL)
+    }
+
+    // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testDropPingReply() {
+        // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
+        // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
+        // should be turned on.
+        assume().that(getVsrApiLevel()).isAtLeast(34)
+        assumeApfVersionSupportAtLeast(4)
+
+        // clear any active APF filter
+        clearApfMemory()
+        readProgram() // wait for install completion
+
+        // Assert that initial ping does not get filtered.
+        val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            68
+        } else {
+            4
+        }
+        val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        packetReader.sendPing(data, payloadSize)
+        assertThat(packetReader.expectPingReply()).isEqualTo(data)
+
+        // Generate an APF program that drops the next ping
+        val gen = ApfV4Generator(
+                caps.apfVersionSupported,
+                caps.maximumApfProgramSize,
+                caps.maximumApfProgramSize
+        )
+
+        // If not ICMPv6 Echo Reply -> PASS
+        gen.addPassIfNotIcmpv6EchoReply()
+
+        // if not data matches -> PASS
+        gen.addLoadImmediate(R0, ICMP6_TYPE_OFFSET + PING_HEADER_LENGTH)
+        gen.addJumpIfBytesAtR0NotEqual(data, BaseApfGenerator.PASS_LABEL)
+
+        // else DROP
+        gen.addJump(BaseApfGenerator.DROP_LABEL)
+
+        val program = gen.generate()
+        installAndVerifyProgram(program)
+
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingDropped()
+    }
+
+    fun clearApfMemory() = installProgram(ByteArray(caps.maximumApfProgramSize))
+
+    // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testPrefilledMemorySlotsV4() {
+        // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
+        // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
+        // should be turned on.
+        assume().that(getVsrApiLevel()).isAtLeast(34)
+        // Test v4 memory slots on both v4 and v6 interpreters.
+        assumeApfVersionSupportAtLeast(4)
+        clearApfMemory()
+        val gen = ApfV4Generator(
+                caps.apfVersionSupported,
+                caps.maximumApfProgramSize,
+                caps.maximumApfProgramSize
+        )
+
+        // If not ICMPv6 Echo Reply -> PASS
+        gen.addPassIfNotIcmpv6EchoReply()
+
+        // Store all prefilled memory slots in counter region [500, 520)
+        val counterRegion = 500
+        gen.addLoadImmediate(R1, counterRegion)
+        gen.addLoadFromMemory(R0, MemorySlot.PROGRAM_SIZE)
+        gen.addStoreData(R0, 0)
+        gen.addLoadFromMemory(R0, MemorySlot.RAM_LEN)
+        gen.addStoreData(R0, 4)
+        gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE)
+        gen.addStoreData(R0, 8)
+        gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+        gen.addStoreData(R0, 12)
+        gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS)
+        gen.addStoreData(R0, 16)
+
+        val program = gen.generate()
+        assertThat(program.size).isLessThan(counterRegion)
+        installAndVerifyProgram(program)
+
+        // Trigger the program by sending a ping and waiting on the reply.
+        val payloadSize = if (getFirstApiLevel() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            68
+        } else {
+            4
+        }
+        val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        val readResult = readProgram()
+        val buffer = ByteBuffer.wrap(readResult, counterRegion, 20 /* length */)
+        expect.withMessage("PROGRAM_SIZE").that(buffer.getInt()).isEqualTo(program.size)
+        expect.withMessage("RAM_LEN").that(buffer.getInt()).isEqualTo(caps.maximumApfProgramSize)
+        expect.withMessage("IPV4_HEADER_SIZE").that(buffer.getInt()).isEqualTo(0)
+        // Ping packet payload + ICMPv6 header (8)  + IPv6 header (40) + ethernet header (14)
+        expect.withMessage("PACKET_SIZE").that(buffer.getInt()).isEqualTo(payloadSize + 8 + 40 + 14)
+        expect.withMessage("FILTER_AGE_SECONDS").that(buffer.getInt()).isLessThan(5)
+    }
+
+    // APF integration is mostly broken before V
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testFilterAgeIncreasesBetweenPackets() {
+        // VSR-14 mandates APF to be turned on when the screen is off and the Wi-Fi link
+        // is idle or traffic is less than 10 Mbps. Before that, we don't mandate when the APF
+        // should be turned on.
+        assume().that(getVsrApiLevel()).isAtLeast(34)
+        assumeApfVersionSupportAtLeast(4)
+        clearApfMemory()
+        val gen = ApfV4Generator(
+                caps.apfVersionSupported,
+                caps.maximumApfProgramSize,
+                caps.maximumApfProgramSize
+        )
+
+        // If not ICMPv6 Echo Reply -> PASS
+        gen.addPassIfNotIcmpv6EchoReply()
+
+        // Store all prefilled memory slots in counter region [500, 520)
+        val counterRegion = 500
+        gen.addLoadImmediate(R1, counterRegion)
+        gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS)
+        gen.addStoreData(R0, 0)
+
+        installAndVerifyProgram(gen.generate())
+
+        val payloadSize = 56
+        val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        var buffer = ByteBuffer.wrap(readProgram(), counterRegion, 4 /* length */)
+        val filterAgeSecondsOrig = buffer.getInt()
+
+        Thread.sleep(5100)
+
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        buffer = ByteBuffer.wrap(readProgram(), counterRegion, 4 /* length */)
+        val filterAgeSeconds = buffer.getInt()
+        // Assert that filter age has increased, but not too much.
+        val timeDiff = filterAgeSeconds - filterAgeSecondsOrig
+        assertThat(timeDiff).isAnyOf(5, 6)
+    }
+
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
+    @Test
+    fun testFilterAge16384thsIncreasesBetweenPackets() {
+        assumeApfVersionSupportAtLeast(6000)
+        clearApfMemory()
+        val gen = ApfV6Generator(
+                caps.apfVersionSupported,
+                caps.maximumApfProgramSize,
+                caps.maximumApfProgramSize
+        )
+
+        // If not ICMPv6 Echo Reply -> PASS
+        gen.addPassIfNotIcmpv6EchoReply()
+
+        // Store all prefilled memory slots in counter region [500, 520)
+        gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_16384THS)
+        gen.addStoreCounter(FILTER_AGE_16384THS, R0)
+
+        installAndVerifyProgram(gen.generate())
+
+        val payloadSize = 56
+        val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        var apfRam = readProgram()
+        val filterAge16384thSecondsOrig =
+                ApfCounterTracker.getCounterValue(apfRam, FILTER_AGE_16384THS)
+
+        Thread.sleep(5000)
+
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        apfRam = readProgram()
+        val filterAge16384thSeconds = ApfCounterTracker.getCounterValue(apfRam, FILTER_AGE_16384THS)
+        val timeDiff = (filterAge16384thSeconds - filterAge16384thSecondsOrig)
+        // Expect the HAL plus ping latency to be less than 800ms.
+        val timeDiffLowerBound = (4.99 * 16384).toInt()
+        val timeDiffUpperBound = (5.81 * 16384).toInt()
+        // Assert that filter age has increased, but not too much.
+        assertThat(timeDiff).isGreaterThan(timeDiffLowerBound)
+        assertThat(timeDiff).isLessThan(timeDiffUpperBound)
+    }
+
+    @VsrTest(
+            requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005", "VSR-5.3.12-012", "VSR-5.3.12-013",
+                "VSR-5.3.12-014", "VSR-5.3.12-015", "VSR-5.3.12-016", "VSR-5.3.12-017"]
+    )
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testReplyPing() {
+        assumeApfVersionSupportAtLeast(6000)
+        installProgram(ByteArray(caps.maximumApfProgramSize) { 0 }) // Clear previous program
+        readProgram() // Ensure installation is complete
+
+        val payloadSize = 56
+        val payload = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        val firstByte = payload.take(1).toByteArray()
+
+        val pingRequestIpv6PayloadLen = PING_HEADER_LENGTH + 1
+        val pingRequestPktLen = ETH_HEADER_LEN + IPV6_HEADER_LEN + pingRequestIpv6PayloadLen
+
+        val gen = ApfV6Generator(
+                caps.apfVersionSupported,
+                caps.maximumApfProgramSize,
+                caps.maximumApfProgramSize
+        )
+        val skipPacketLabel = gen.uniqueLabel
+
+        // Summary of the program:
+        //   if the packet is not ICMPv6 echo reply
+        //     pass
+        //   else if the echo reply payload size is 1
+        //     increase PASSED_IPV6_ICMP counter
+        //     pass
+        //   else
+        //     transmit a ICMPv6 echo request packet with the first byte of the payload in the reply
+        //     increase DROPPED_IPV6_MULTICAST_PING counter
+        //     drop
+        val program = gen
+                .addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+                .addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), skipPacketLabel)
+                .addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+                .addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), skipPacketLabel)
+                .addLoad8(R0, ICMP6_TYPE_OFFSET)
+                .addJumpIfR0NotEquals(0x81, skipPacketLabel) // Echo reply type
+                .addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+                .addCountAndPassIfR0Equals(
+                        (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
+                                .toLong(),
+                        PASSED_IPV6_ICMP
+                )
+                // Ping Packet Generation
+                .addAllocate(pingRequestPktLen)
+                // Eth header
+                .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
+                .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
+                .addWriteU16(ETH_P_IPV6) // IPv6 type
+                // IPv6 Header
+                .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
+                // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
+                .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
+                .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
+                .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
+                // ICMPv6
+                .addWriteU8(0x80) // type: echo request
+                .addWriteU8(0) // code
+                .addWriteU16(pingRequestIpv6PayloadLen) // checksum
+                // identifier
+                .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
+                .addWriteU16(0) // sequence number
+                .addDataCopy(firstByte) // data
+                .addTransmitL4(
+                        ETHER_HEADER_LEN, // ip_ofs
+                        ICMP6_CHECKSUM_OFFSET, // csum_ofs
+                        IPV6_SRC_ADDR_OFFSET, // csum_start
+                        IPPROTO_ICMPV6, // partial_sum
+                        false // udp
+                )
+                // Warning: the program abuse DROPPED_IPV6_MULTICAST_PING for debugging purpose
+                .addCountAndDrop(DROPPED_IPV6_MULTICAST_PING)
+                .defineLabel(skipPacketLabel)
+                .addPass()
+                .generate()
+
+        installAndVerifyProgram(program)
+
+        packetReader.sendPing(payload, payloadSize)
+
+        val replyPayload = try {
+            packetReader.expectPingReply(TIMEOUT_MS * 2)
+        } catch (e: TimeoutException) {
+            byteArrayOf() // Empty payload if timeout occurs
+        }
+
+        val apfCounterTracker = ApfCounterTracker()
+        apfCounterTracker.updateCountersFromData(readProgram())
+        Log.i(TAG, "counter map: ${apfCounterTracker.counters}")
+
+        assertThat(replyPayload).isEqualTo(firstByte)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
index 3e5d0ba..16a7b73 100644
--- a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
@@ -48,6 +48,7 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
 import com.android.testutils.DevSdkIgnoreRule;
 
 import org.junit.Before;
@@ -67,7 +68,10 @@
 @RunWith(AndroidJUnit4.class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // BatteryStatsManager did not exist on Q
 public class BatteryStatsManagerTest{
-    @Rule
+    @Rule(order = 1)
+    public final AutoReleaseNetworkCallbackRule
+            networkCallbackRule = new AutoReleaseNetworkCallbackRule();
+    @Rule(order = 2)
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
     private static final String TAG = BatteryStatsManagerTest.class.getSimpleName();
     private static final String TEST_URL = "https://connectivitycheck.gstatic.com/generate_204";
@@ -145,7 +149,7 @@
             return;
         }
 
-        final Network cellNetwork = mCtsNetUtils.connectToCell();
+        final Network cellNetwork = networkCallbackRule.requestCell();
         final URL url = new URL(TEST_URL);
 
         // Get cellular battery stats
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 99222dd..07e2024 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -47,6 +47,7 @@
 import com.android.modules.utils.build.SdkLevel.isAtLeastR
 import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
 import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
+import com.android.testutils.AutoReleaseNetworkCallbackRule
 import com.android.testutils.DeviceConfigRule
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.SkipMainlinePresubmit
@@ -101,9 +102,12 @@
 
     private val server = TestHttpServer("localhost")
 
-    @get:Rule
+    @get:Rule(order = 1)
     val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
 
+    @get:Rule(order = 2)
+    val networkCallbackRule = AutoReleaseNetworkCallbackRule()
+
     companion object {
         @JvmStatic @BeforeClass
         fun setUpClass() {
@@ -144,15 +148,15 @@
         assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
         assumeFalse(pm.hasSystemFeature(FEATURE_WATCH))
         utils.ensureWifiConnected()
-        val cellNetwork = utils.connectToCell()
+        val cellNetwork = networkCallbackRule.requestCell()
 
         // Verify cell network is validated
         val cellReq = NetworkRequest.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .build()
-        val cellCb = TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS)
-        cm.registerNetworkCallback(cellReq, cellCb)
+        val cellCb = networkCallbackRule.registerNetworkCallback(cellReq,
+            TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS))
         val cb = cellCb.poll { it.network == cellNetwork &&
                 it is CapabilitiesChanged && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
         }
@@ -213,8 +217,6 @@
         } finally {
             cm.unregisterNetworkCallback(wifiCb)
             server.stop()
-            // disconnectFromCell should be called after connectToCell
-            utils.disconnectFromCell()
         }
     }
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index cdf8340..88c2d5a 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -36,11 +36,22 @@
 import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISABLED;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_OEM_DENY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_RESTRICTED_MODE;
 import static android.net.ConnectivityManager.EXTRA_NETWORK;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_REQUEST;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
@@ -71,7 +82,9 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -81,7 +94,6 @@
 import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
 import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
 import static android.net.cts.util.CtsNetUtils.TEST_HOST;
-import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
 import static android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
 import static android.os.Process.INVALID_UID;
@@ -162,6 +174,7 @@
 import android.net.wifi.WifiManager;
 import android.os.Binder;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.MessageQueue;
@@ -180,10 +193,10 @@
 import android.util.Log;
 import android.util.Range;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.RequiresDevice;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.DynamicConfigDeviceSide;
 import com.android.internal.util.ArrayUtils;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
@@ -191,15 +204,18 @@
 import com.android.networkstack.apishim.ConstantsShim;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
 import com.android.testutils.CompatUtil;
+import com.android.testutils.ConnectUtil;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.DeviceConfigRule;
 import com.android.testutils.DeviceInfoUtils;
 import com.android.testutils.DumpTestUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
-import com.android.testutils.SkipMainlinePresubmit;
 import com.android.testutils.SkipPresubmit;
 import com.android.testutils.TestHttpServer;
 import com.android.testutils.TestNetworkTracker;
@@ -211,6 +227,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -234,6 +251,7 @@
 import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
@@ -257,12 +275,17 @@
 import fi.iki.elonen.NanoHTTPD.Response.IStatus;
 import fi.iki.elonen.NanoHTTPD.Response.Status;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
 public class ConnectivityManagerTest {
-    @Rule
+    @Rule(order = 1)
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
-    @Rule
+    @Rule(order = 2)
+    public final AutoReleaseNetworkCallbackRule
+            networkCallbackRule = new AutoReleaseNetworkCallbackRule();
+
+    @Rule(order = 3)
     public final DeviceConfigRule mTestValidationConfigRule = new DeviceConfigRule(
             5 /* retryCountBeforeSIfConfigChanged */);
 
@@ -290,6 +313,7 @@
 
     // Airplane Mode BroadcastReceiver Timeout
     private static final long AIRPLANE_MODE_CHANGE_TIMEOUT_MS = 10_000L;
+    private static final long CELL_DATA_AVAILABLE_TIMEOUT_MS = 120_000L;
 
     // Timeout for applying uids allowed on restricted networks
     private static final long APPLYING_UIDS_ALLOWED_ON_RESTRICTED_NETWORKS_TIMEOUT_MS = 3_000L;
@@ -315,6 +339,11 @@
     private static final String TEST_HTTPS_URL_PATH = "/https_path";
     private static final String TEST_HTTP_URL_PATH = "/http_path";
     private static final String LOCALHOST_HOSTNAME = "localhost";
+    private static final String TEST_MODULE_NAME_OPTION = "test-module-name";
+    private static final String IP_ADDRESS_ECHO_URL_KEY = "IP_ADDRESS_ECHO_URL";
+    private static final List<String> ALLOWED_IP_ADDRESS_ECHO_URLS = Arrays.asList(
+            "https://google-ipv6test.appspot.com/ip.js?fmt=text",
+            "https://ipv6test.googleapis-cn.com/ip.js?fmt=text");
     // Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
     private static final long WIFI_CONNECT_TIMEOUT_MS = 60_000L;
 
@@ -328,8 +357,6 @@
     private final ArraySet<Integer> mNetworkTypes = new ArraySet<>();
     private UiAutomation mUiAutomation;
     private CtsNetUtils mCtsNetUtils;
-    // The registered callbacks.
-    private List<NetworkCallback> mRegisteredCallbacks = new ArrayList<>();
     // Used for cleanup purposes.
     private final List<Range<Integer>> mVpnRequiredUidRanges = new ArrayList<>();
 
@@ -411,11 +438,6 @@
 
     @After
     public void tearDown() throws Exception {
-        // Release any NetworkRequests filed to connect mobile data.
-        if (mCtsNetUtils.cellConnectAttempted()) {
-            mCtsNetUtils.disconnectFromCell();
-        }
-
         if (TestUtils.shouldTestSApis()) {
             runWithShellPermissionIdentity(
                     () -> mCmShim.setRequireVpnForUids(false, mVpnRequiredUidRanges),
@@ -425,15 +447,12 @@
         // All tests in this class require a working Internet connection as they start. Make
         // sure there is still one as they end that's ready to use for the next test to use.
         mTestValidationConfigRule.runAfterNextCleanup(() -> {
-            final TestNetworkCallback callback = new TestNetworkCallback();
-            registerDefaultNetworkCallback(callback);
-            try {
-                assertNotNull("Couldn't restore Internet connectivity",
-                        callback.waitForAvailable());
-            } finally {
-                // Unregister all registered callbacks.
-                unregisterRegisteredCallbacks();
-            }
+            // mTestValidationConfigRule has higher order than networkCallbackRule, so
+            // networkCallbackRule is the outer rule and will be cleaned up after this method.
+            final TestableNetworkCallback callback =
+                    networkCallbackRule.registerDefaultNetworkCallback();
+            assertNotNull("Couldn't restore Internet connectivity",
+                    callback.eventuallyExpect(CallbackEntry.AVAILABLE));
         });
     }
 
@@ -555,7 +574,7 @@
             throws InterruptedException {
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
         // Make sure cell is active to retrieve IMSI for verification in later step.
-        final Network cellNetwork = mCtsNetUtils.connectToCell();
+        final Network cellNetwork = networkCallbackRule.requestCell();
         final String subscriberId = getSubscriberIdForCellNetwork(cellNetwork);
         assertFalse(TextUtils.isEmpty(subscriberId));
 
@@ -845,7 +864,7 @@
      * Tests that connections can be opened on WiFi and cellphone networks,
      * and that they are made from different IP addresses.
      */
-    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @AppModeFull(reason = "Cannot get WifiManager or access the SD card in instant app mode")
     @Test
     @RequiresDevice // Virtual devices use a single internet connection for all networks
     public void testOpenConnection() throws Exception {
@@ -853,9 +872,10 @@
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
 
         Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
-        Network cellNetwork = mCtsNetUtils.connectToCell();
+        Network cellNetwork = networkCallbackRule.requestCell();
         // This server returns the requestor's IP address as the response body.
-        URL url = new URL("http://google-ipv6test.appspot.com/ip.js?fmt=text");
+        String ipAddressEchoUrl = getIpAddressEchoUrlFromConfig();
+        URL url = new URL(ipAddressEchoUrl);
         String wifiAddressString = httpGet(wifiNetwork, url);
         String cellAddressString = httpGet(cellNetwork, url);
 
@@ -872,6 +892,19 @@
     }
 
     /**
+     * Gets IP address echo url from dynamic config.
+     */
+    private static String getIpAddressEchoUrlFromConfig() throws Exception {
+        Bundle instrumentationArgs = InstrumentationRegistry.getArguments();
+        String testModuleName = instrumentationArgs.getString(TEST_MODULE_NAME_OPTION);
+        // Get the DynamicConfig.xml contents and extract the ipv6 test URL.
+        DynamicConfigDeviceSide dynamicConfig = new DynamicConfigDeviceSide(testModuleName);
+        String ipAddressEchoUrl = dynamicConfig.getValue(IP_ADDRESS_ECHO_URL_KEY);
+        assertContains(ALLOWED_IP_ADDRESS_ECHO_URLS, ipAddressEchoUrl);
+        return ipAddressEchoUrl;
+    }
+
+    /**
      * Performs a HTTP GET to the specified URL on the specified Network, and returns
      * the response body decoded as UTF-8.
      */
@@ -993,10 +1026,10 @@
         // default network.
         return new NetworkRequest.Builder()
                 .clearCapabilities()
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .addCapability(NET_CAPABILITY_TRUSTED)
+                .addCapability(NET_CAPABILITY_NOT_VPN)
+                .addCapability(NET_CAPABILITY_INTERNET)
                 .build();
     }
 
@@ -1022,15 +1055,14 @@
 
     @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
-    @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
     public void testIsPrivateDnsBroken() throws InterruptedException {
         final String invalidPrivateDnsServer = "invalidhostname.example.com";
         final String goodPrivateDnsServer = "dns.google";
         mCtsNetUtils.storePrivateDnsSetting();
-        final TestableNetworkCallback cb = new TestableNetworkCallback();
         final NetworkRequest networkRequest = new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_INTERNET).build();
-        registerNetworkCallback(networkRequest, cb);
+        final TestableNetworkCallback cb =
+                networkCallbackRule.registerNetworkCallback(networkRequest);
         final Network networkForPrivateDns = mCm.getActiveNetwork();
         try {
             // Verifying the good private DNS sever
@@ -1068,24 +1100,27 @@
         assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         // We will register for a WIFI network being available or lost.
-        final TestNetworkCallback callback = new TestNetworkCallback();
-        registerNetworkCallback(makeWifiNetworkRequest(), callback);
+        final TestableNetworkCallback callback = networkCallbackRule.registerNetworkCallback(
+                makeWifiNetworkRequest());
 
-        final TestNetworkCallback defaultTrackingCallback = new TestNetworkCallback();
-        registerDefaultNetworkCallback(defaultTrackingCallback);
+        final TestableNetworkCallback defaultTrackingCallback =
+                networkCallbackRule.registerDefaultNetworkCallback();
 
-        final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
-        final TestNetworkCallback perUidCallback = new TestNetworkCallback();
-        final TestNetworkCallback bestMatchingCallback = new TestNetworkCallback();
+        final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();
+        final TestableNetworkCallback perUidCallback = new TestableNetworkCallback();
+        final TestableNetworkCallback bestMatchingCallback = new TestableNetworkCallback();
         final Handler h = new Handler(Looper.getMainLooper());
         if (TestUtils.shouldTestSApis()) {
             assertThrows(SecurityException.class, () ->
-                    registerSystemDefaultNetworkCallback(systemDefaultCallback, h));
+                    networkCallbackRule.registerSystemDefaultNetworkCallback(
+                            systemDefaultCallback, h));
             runWithShellPermissionIdentity(() -> {
-                registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
-                registerDefaultNetworkCallbackForUid(Process.myUid(), perUidCallback, h);
+                networkCallbackRule.registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
+                networkCallbackRule.registerDefaultNetworkCallbackForUid(Process.myUid(),
+                        perUidCallback, h);
             }, NETWORK_SETTINGS);
-            registerBestMatchingNetworkCallback(makeDefaultRequest(), bestMatchingCallback, h);
+            networkCallbackRule.registerBestMatchingNetworkCallback(
+                    makeDefaultRequest(), bestMatchingCallback, h);
         }
 
         Network wifiNetwork = null;
@@ -1094,24 +1129,22 @@
         // Now we should expect to get a network callback about availability of the wifi
         // network even if it was already connected as a state-based action when the callback
         // is registered.
-        wifiNetwork = callback.waitForAvailable();
+        wifiNetwork = callback.eventuallyExpect(CallbackEntry.AVAILABLE).getNetwork();
         assertNotNull("Did not receive onAvailable for TRANSPORT_WIFI request",
                 wifiNetwork);
 
-        final Network defaultNetwork = defaultTrackingCallback.waitForAvailable();
+        final Network defaultNetwork = defaultTrackingCallback.eventuallyExpect(
+                CallbackEntry.AVAILABLE).getNetwork();
         assertNotNull("Did not receive onAvailable on default network callback",
                 defaultNetwork);
 
         if (TestUtils.shouldTestSApis()) {
-            assertNotNull("Did not receive onAvailable on system default network callback",
-                    systemDefaultCallback.waitForAvailable());
-            final Network perUidNetwork = perUidCallback.waitForAvailable();
-            assertNotNull("Did not receive onAvailable on per-UID default network callback",
-                    perUidNetwork);
+            systemDefaultCallback.eventuallyExpect(CallbackEntry.AVAILABLE);
+            final Network perUidNetwork = perUidCallback.eventuallyExpect(CallbackEntry.AVAILABLE)
+                    .getNetwork();
             assertEquals(defaultNetwork, perUidNetwork);
-            final Network bestMatchingNetwork = bestMatchingCallback.waitForAvailable();
-            assertNotNull("Did not receive onAvailable on best matching network callback",
-                    bestMatchingNetwork);
+            final Network bestMatchingNetwork = bestMatchingCallback.eventuallyExpect(
+                    CallbackEntry.AVAILABLE).getNetwork();
             assertEquals(defaultNetwork, bestMatchingNetwork);
         }
     }
@@ -1123,8 +1156,8 @@
         final Handler h = new Handler(Looper.getMainLooper());
         // Verify registerSystemDefaultNetworkCallback can be accessed via
         // CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
-        runWithShellPermissionIdentity(() ->
-                        registerSystemDefaultNetworkCallback(new TestNetworkCallback(), h),
+        runWithShellPermissionIdentity(
+                () -> networkCallbackRule.registerSystemDefaultNetworkCallback(h),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS);
     }
 
@@ -1226,13 +1259,14 @@
             final IntentFilter filter = new IntentFilter();
             filter.addAction(broadcastAction);
 
+            final CompletableFuture<NetworkRequest> requestFuture = new CompletableFuture<>();
             final CompletableFuture<Network> networkFuture = new CompletableFuture<>();
             final AtomicInteger receivedCount = new AtomicInteger(0);
             receiver = new BroadcastReceiver() {
                 @Override
                 public void onReceive(Context context, Intent intent) {
                     final NetworkRequest request = intent.getParcelableExtra(EXTRA_NETWORK_REQUEST);
-                    assertPendingIntentRequestMatches(request, secondRequest, useListen);
+                    requestFuture.complete(request);
                     receivedCount.incrementAndGet();
                     networkFuture.complete(intent.getParcelableExtra(EXTRA_NETWORK));
                 }
@@ -1247,6 +1281,9 @@
             } catch (TimeoutException e) {
                 throw new AssertionError("PendingIntent not received for " + secondRequest, e);
             }
+            assertPendingIntentRequestMatches(
+                    requestFuture.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS),
+                    secondRequest, useListen);
 
             // Sleep for a small amount of time to try to check that only one callback is ever
             // received (so the first callback was really unregistered). This does not guarantee
@@ -1294,15 +1331,14 @@
      */
     @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
     @Test
-    public void testRequestNetworkCallback() throws Exception {
-        final TestNetworkCallback callback = new TestNetworkCallback();
-        requestNetwork(new NetworkRequest.Builder()
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-                .build(), callback);
+    public void testRequestNetworkCallback() {
+        final TestableNetworkCallback callback = networkCallbackRule.requestNetwork(
+                new NetworkRequest.Builder().addCapability(
+                                NET_CAPABILITY_INTERNET)
+                        .build());
 
         // Wait to get callback for availability of internet
-        Network internetNetwork = callback.waitForAvailable();
-        assertNotNull("Did not receive NetworkCallback#onAvailable for INTERNET", internetNetwork);
+        callback.eventuallyExpect(CallbackEntry.AVAILABLE).getNetwork();
     }
 
     /**
@@ -1320,16 +1356,13 @@
             }
         }
 
-        final TestNetworkCallback callback = new TestNetworkCallback();
-        requestNetwork(new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(),
-                callback, 100);
 
+        final TestableNetworkCallback callback = networkCallbackRule.requestNetwork(
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(),
+                100 /* timeoutMs */);
         try {
             // Wait to get callback for unavailability of requested network
-            assertTrue("Did not receive NetworkCallback#onUnavailable",
-                    callback.waitForUnavailable());
-        } catch (InterruptedException e) {
-            fail("NetworkCallback wait was interrupted.");
+            callback.eventuallyExpect(CallbackEntry.UNAVAILABLE, 2_000 /* timeoutMs */);
         } finally {
             if (previousWifiEnabledState) {
                 mCtsNetUtils.connectToWifi();
@@ -1416,40 +1449,48 @@
             final boolean useSystemDefault)
             throws Exception {
         final CompletableFuture<Network> networkFuture = new CompletableFuture<>();
-        final NetworkCallback networkCallback = new NetworkCallback() {
-            @Override
-            public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
-                if (!nc.hasTransport(targetTransportType)) return;
 
-                final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
-                final boolean validated = nc.hasCapability(NET_CAPABILITY_VALIDATED);
-                if (metered == requestedMeteredness && (!waitForValidation || validated)) {
-                    networkFuture.complete(network);
+        // Registering a callback here guarantees onCapabilitiesChanged is called immediately
+        // with the current setting. Therefore, if the setting has already been changed,
+        // this method will return right away, and if not, it'll wait for the setting to change.
+        final TestableNetworkCallback networkCallback;
+        if (useSystemDefault) {
+            networkCallback = runWithShellPermissionIdentity(() -> {
+                if (isAtLeastS()) {
+                    return networkCallbackRule.registerSystemDefaultNetworkCallback(
+                            new Handler(Looper.getMainLooper()));
+                } else {
+                    // registerSystemDefaultNetworkCallback is only supported on S+.
+                    return networkCallbackRule.requestNetwork(
+                            new NetworkRequest.Builder()
+                                    .clearCapabilities()
+                                    .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                                    .addCapability(NET_CAPABILITY_TRUSTED)
+                                    .addCapability(NET_CAPABILITY_NOT_VPN)
+                                    .addCapability(NET_CAPABILITY_INTERNET)
+                                    .build(),
+                            new TestableNetworkCallback(),
+                            new Handler(Looper.getMainLooper()));
                 }
-            }
-        };
-
-        try {
-            // Registering a callback here guarantees onCapabilitiesChanged is called immediately
-            // with the current setting. Therefore, if the setting has already been changed,
-            // this method will return right away, and if not, it'll wait for the setting to change.
-            if (useSystemDefault) {
-                runWithShellPermissionIdentity(() ->
-                                registerSystemDefaultNetworkCallback(networkCallback,
-                                        new Handler(Looper.getMainLooper())),
-                        NETWORK_SETTINGS);
-            } else {
-                registerDefaultNetworkCallback(networkCallback);
-            }
-
-            // Changing meteredness on wifi involves reconnecting, which can take several seconds
-            // (involves re-associating, DHCP...).
-            return networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-        } catch (TimeoutException e) {
-            throw new AssertionError("Timed out waiting for active network metered status to "
-                    + "change to " + requestedMeteredness + " ; network = "
-                    + mCm.getActiveNetwork(), e);
+            },
+            NETWORK_SETTINGS);
+        } else {
+            networkCallback = networkCallbackRule.registerDefaultNetworkCallback();
         }
+
+        return networkCallback.eventuallyExpect(
+                CallbackEntry.NETWORK_CAPS_UPDATED,
+                // Changing meteredness on wifi involves reconnecting, which can take several
+                // seconds (involves re-associating, DHCP...).
+                NETWORK_CALLBACK_TIMEOUT_MS,
+                cb -> {
+                    final NetworkCapabilities nc = cb.getCaps();
+                    if (!nc.hasTransport(targetTransportType)) return false;
+
+                    final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
+                    final boolean validated = nc.hasCapability(NET_CAPABILITY_VALIDATED);
+                    return metered == requestedMeteredness && (!waitForValidation || validated);
+                }).getNetwork();
     }
 
     private Network setWifiMeteredStatusAndWait(String ssid, boolean isMetered,
@@ -2025,7 +2066,7 @@
             return;
         }
 
-        final Network network = mCtsNetUtils.connectToCell();
+        final Network network = networkCallbackRule.requestCell();
         final int supported = getSupportedKeepalivesForNet(network);
         final InetAddress srcAddr = getFirstV4Address(network);
         assumeTrue("This test requires native IPv4", srcAddr != null);
@@ -2091,15 +2132,15 @@
     }
 
     private void verifyBindSocketToRestrictedNetworkDisallowed() throws Exception {
-        final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         TEST_RESTRICTED_NW_IFACE_NAME))
                 .build();
-        runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
+        final TestableNetworkCallback testNetworkCb = runWithShellPermissionIdentity(
+                () -> networkCallbackRule.requestNetwork(testRequest),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS,
                 // CONNECTIVITY_INTERNAL is for requesting restricted network because shell does not
                 // have CONNECTIVITY_USE_RESTRICTED_NETWORKS on R.
@@ -2115,7 +2156,7 @@
                     NETWORK_CALLBACK_TIMEOUT_MS,
                     entry -> network.equals(entry.getNetwork())
                             && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                            .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+                            .hasCapability(NET_CAPABILITY_NOT_RESTRICTED)));
             // CtsNetTestCases package doesn't hold CONNECTIVITY_USE_RESTRICTED_NETWORKS, so it
             // does not allow to bind socket to restricted network.
             assertThrows(IOException.class, () -> network.bindSocket(socket));
@@ -2200,8 +2241,7 @@
             registerCallbackAndWaitForAvailable(makeWifiNetworkRequest(), wifiCb);
         }
         if (supportTelephony) {
-            // connectToCell needs to be followed by disconnectFromCell, which is called in tearDown
-            mCtsNetUtils.connectToCell();
+            networkCallbackRule.requestCell();
             registerCallbackAndWaitForAvailable(makeCellNetworkRequest(), telephonyCb);
         }
 
@@ -2229,7 +2269,10 @@
             // connectToCell only registers a request, it cannot / does not need to be called twice
             mCtsNetUtils.ensureWifiConnected();
             if (verifyWifi) waitForAvailable(wifiCb);
-            if (supportTelephony) waitForAvailable(telephonyCb);
+            if (supportTelephony) {
+                telephonyCb.eventuallyExpect(
+                        CallbackEntry.AVAILABLE, CELL_DATA_AVAILABLE_TIMEOUT_MS);
+            }
         } finally {
             // Restore the previous state of airplane mode and permissions:
             runShellCommand("cmd connectivity airplane-mode "
@@ -2239,7 +2282,7 @@
 
     private void registerCallbackAndWaitForAvailable(@NonNull final NetworkRequest request,
             @NonNull final TestableNetworkCallback cb) {
-        registerNetworkCallback(request, cb);
+        networkCallbackRule.registerNetworkCallback(request, cb);
         waitForAvailable(cb);
     }
 
@@ -2347,21 +2390,13 @@
 
     private void verifySsidFromCallbackNetworkCapabilities(@NonNull String ssid, boolean hasSsid)
             throws Exception {
-        final CompletableFuture<NetworkCapabilities> foundNc = new CompletableFuture();
-        final NetworkCallback callback = new NetworkCallback() {
-            @Override
-            public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
-                foundNc.complete(nc);
-            }
-        };
-
-        registerNetworkCallback(makeWifiNetworkRequest(), callback);
+        final TestableNetworkCallback callback =
+                networkCallbackRule.registerNetworkCallback(makeWifiNetworkRequest());
         // Registering a callback here guarantees onCapabilitiesChanged is called immediately
         // because WiFi network should be connected.
-        final NetworkCapabilities nc =
-                foundNc.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        final NetworkCapabilities nc = callback.eventuallyExpect(
+                CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS).getCaps();
         // Verify if ssid is contained in the NetworkCapabilities received from callback.
-        assertNotNull("NetworkCapabilities of the network is null", nc);
         assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find());
     }
 
@@ -2390,8 +2425,8 @@
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(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)
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         testNetworkInterface.getInterfaceName()))
                 .build();
@@ -2400,14 +2435,15 @@
         final TestableNetworkCallback callback = new TestableNetworkCallback();
         final Handler handler = new Handler(Looper.getMainLooper());
         assertThrows(SecurityException.class,
-                () -> requestBackgroundNetwork(testRequest, callback, handler));
+                () -> networkCallbackRule.requestBackgroundNetwork(testRequest, callback, handler));
 
         Network testNetwork = null;
         try {
             // Request background test network via Shell identity which has NETWORK_SETTINGS
             // permission granted.
             runWithShellPermissionIdentity(
-                    () -> requestBackgroundNetwork(testRequest, callback, handler),
+                    () -> networkCallbackRule.requestBackgroundNetwork(
+                            testRequest, callback, handler),
                     new String[] { android.Manifest.permission.NETWORK_SETTINGS });
 
             // Register the test network agent which has no foreground request associated to it.
@@ -2478,8 +2514,11 @@
         }
     }
 
+    // On V+, ConnectivityService generates blockedReasons based on bpf map contents even if the
+    // otherUid does not exist on device. So if allowlist chain (e.g. background chain) is enabled,
+    // blockedReasons for otherUid will not be BLOCKED_REASON_NONE.
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
-    @Test
+    @Test @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testBlockedStatusCallback() throws Exception {
         // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
         // shims, and @IgnoreUpTo does not check that.
@@ -2500,9 +2539,10 @@
         final int otherUid = UserHandle.getUid(5, Process.FIRST_APPLICATION_UID);
         final Handler handler = new Handler(Looper.getMainLooper());
 
-        registerDefaultNetworkCallback(myUidCallback, handler);
-        runWithShellPermissionIdentity(() -> registerDefaultNetworkCallbackForUid(
-                otherUid, otherUidCallback, handler), NETWORK_SETTINGS);
+        networkCallbackRule.registerDefaultNetworkCallback(myUidCallback, handler);
+        runWithShellPermissionIdentity(
+                () -> networkCallbackRule.registerDefaultNetworkCallbackForUid(
+                        otherUid, otherUidCallback, handler), NETWORK_SETTINGS);
 
         final Network defaultNetwork = myUidCallback.expect(CallbackEntry.AVAILABLE).getNetwork();
         final List<DetailedBlockedStatusCallback> allCallbacks =
@@ -2558,14 +2598,14 @@
         assertNotNull(info);
         assertEquals(DetailedState.CONNECTED, info.getDetailedState());
 
-        final TestableNetworkCallback callback = new TestableNetworkCallback();
+        final TestableNetworkCallback callback;
         try {
             mCmShim.setLegacyLockdownVpnEnabled(true);
 
             // setLegacyLockdownVpnEnabled is asynchronous and only takes effect when the
             // ConnectivityService handler thread processes it. Ensure it has taken effect by doing
             // something that blocks until the handler thread is idle.
-            registerDefaultNetworkCallback(callback);
+            callback = networkCallbackRule.registerDefaultNetworkCallback();
             waitForAvailable(callback);
 
             // Test one of the effects of setLegacyLockdownVpnEnabled: the fact that any NetworkInfo
@@ -2832,9 +2872,9 @@
     private void registerTestOemNetworkPreferenceCallbacks(
             @NonNull final TestableNetworkCallback defaultCallback,
             @NonNull final TestableNetworkCallback systemDefaultCallback) {
-        registerDefaultNetworkCallback(defaultCallback);
+        networkCallbackRule.registerDefaultNetworkCallback(defaultCallback);
         runWithShellPermissionIdentity(() ->
-                registerSystemDefaultNetworkCallback(systemDefaultCallback,
+                networkCallbackRule.registerSystemDefaultNetworkCallback(systemDefaultCallback,
                         new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
     }
 
@@ -2903,20 +2943,10 @@
         // This may also apply to wifi in principle, but in practice methods that mock validation
         // URL all disconnect wifi forcefully anyway, so don't wait for wifi to validate.
         if (mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
-            ensureValidatedNetwork(makeCellNetworkRequest());
+            new ConnectUtil(mContext).ensureCellularValidated();
         }
     }
 
-    private void ensureValidatedNetwork(NetworkRequest request) {
-        final TestableNetworkCallback cb = new TestableNetworkCallback();
-        mCm.registerNetworkCallback(request, cb);
-        cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
-                NETWORK_CALLBACK_TIMEOUT_MS,
-                entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                        .hasCapability(NET_CAPABILITY_VALIDATED));
-        mCm.unregisterNetworkCallback(cb);
-    }
-
     @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
     @Test
     public void testAcceptPartialConnectivity_validatedNetwork() throws Exception {
@@ -2950,18 +2980,18 @@
                         + " unless device supports WiFi",
                 mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
-        final TestNetworkCallback cb = new TestNetworkCallback();
         try {
             // Wait for partial connectivity to be detected on the network
             final Network network = preparePartialConnectivity();
 
-            requestNetwork(makeWifiNetworkRequest(), cb);
+            final TestableNetworkCallback cb = networkCallbackRule.requestNetwork(
+                    makeWifiNetworkRequest());
             runAsShell(NETWORK_SETTINGS, () -> {
                 // The always bit is verified in NetworkAgentTest
                 mCm.setAcceptPartialConnectivity(network, false /* accept */, false /* always */);
             });
             // Reject partial connectivity network should cause the network being torn down
-            assertEquals(network, cb.waitForLost());
+            assertEquals(network, cb.eventuallyExpect(CallbackEntry.LOST).getNetwork());
         } finally {
             mHttpServer.stop();
             // Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
@@ -2989,17 +3019,17 @@
         assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
                         + " unless device supports WiFi and telephony", canRunTest);
 
-        final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
         try {
             // Ensure at least one default network candidate connected.
-            mCtsNetUtils.connectToCell();
+            networkCallbackRule.requestCell();
 
             final Network wifiNetwork = prepareUnvalidatedNetwork();
             // Default network should not be wifi ,but checking that wifi is not the default doesn't
             // guarantee that it won't become the default in the future.
             assertNotEquals(wifiNetwork, mCm.getActiveNetwork());
 
-            registerNetworkCallback(makeWifiNetworkRequest(), wifiCb);
+            final TestableNetworkCallback wifiCb = networkCallbackRule.registerNetworkCallback(
+                    makeWifiNetworkRequest());
             runAsShell(NETWORK_SETTINGS, () -> {
                 mCm.setAcceptUnvalidated(wifiNetwork, false /* accept */, false /* always */);
             });
@@ -3026,19 +3056,19 @@
         assumeTrue("testSetAvoidUnvalidated cannot execute"
                 + " unless device supports WiFi and telephony", canRunTest);
 
-        final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
-        final TestableNetworkCallback defaultCb = new TestableNetworkCallback();
         final int previousAvoidBadWifi =
                 ConnectivitySettingsManager.getNetworkAvoidBadWifi(mContext);
 
         allowBadWifi();
 
         try {
-            final Network cellNetwork = mCtsNetUtils.connectToCell();
+            final Network cellNetwork = networkCallbackRule.requestCell();
             final Network wifiNetwork = prepareValidatedNetwork();
 
-            registerDefaultNetworkCallback(defaultCb);
-            registerNetworkCallback(makeWifiNetworkRequest(), wifiCb);
+            final TestableNetworkCallback defaultCb =
+                    networkCallbackRule.registerDefaultNetworkCallback();
+            final TestableNetworkCallback wifiCb = networkCallbackRule.registerNetworkCallback(
+                    makeWifiNetworkRequest());
 
             // Verify wifi is the default network.
             defaultCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
@@ -3101,20 +3131,11 @@
         });
     }
 
-    private Network expectNetworkHasCapability(Network network, int expectedNetCap, long timeout)
-            throws Exception {
-        final CompletableFuture<Network> future = new CompletableFuture();
-        final NetworkCallback cb = new NetworkCallback() {
-            @Override
-            public void onCapabilitiesChanged(Network n, NetworkCapabilities nc) {
-                if (n.equals(network) && nc.hasCapability(expectedNetCap)) {
-                    future.complete(network);
-                }
-            }
-        };
-
-        registerNetworkCallback(new NetworkRequest.Builder().build(), cb);
-        return future.get(timeout, TimeUnit.MILLISECONDS);
+    private Network expectNetworkHasCapability(Network network, int expectedNetCap, long timeout) {
+        return networkCallbackRule.registerNetworkCallback(new NetworkRequest.Builder().build())
+                .eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, timeout,
+                        cb -> cb.getNetwork().equals(network)
+                                && cb.getCaps().hasCapability(expectedNetCap)).getNetwork();
     }
 
     private void prepareHttpServer() throws Exception {
@@ -3214,8 +3235,6 @@
 
         if (supportWifi) {
             mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
-        } else {
-            mCtsNetUtils.disconnectFromCell();
         }
 
         final CompletableFuture<Boolean> future = new CompletableFuture<>();
@@ -3226,7 +3245,7 @@
             if (supportWifi) {
                 mCtsNetUtils.ensureWifiConnected();
             } else {
-                mCtsNetUtils.connectToCell();
+                networkCallbackRule.requestCell();
             }
             assertTrue(future.get(LISTEN_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS));
         }, () -> {
@@ -3267,13 +3286,14 @@
 
         // For testing mobile data preferred uids feature, it needs both wifi and cell network.
         final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
-        final Network cellNetwork = mCtsNetUtils.connectToCell();
-        final TestableNetworkCallback defaultTrackingCb = new TestableNetworkCallback();
-        final TestableNetworkCallback systemDefaultCb = new TestableNetworkCallback();
+        final Network cellNetwork = networkCallbackRule.requestCell();
         final Handler h = new Handler(Looper.getMainLooper());
-        runWithShellPermissionIdentity(() -> registerSystemDefaultNetworkCallback(
-                systemDefaultCb, h), NETWORK_SETTINGS);
-        registerDefaultNetworkCallback(defaultTrackingCb);
+        final TestableNetworkCallback systemDefaultCb = runWithShellPermissionIdentity(
+                () -> networkCallbackRule.registerSystemDefaultNetworkCallback(h),
+                NETWORK_SETTINGS);
+
+        final TestableNetworkCallback defaultTrackingCb =
+                networkCallbackRule.registerDefaultNetworkCallback();
 
         try {
             // CtsNetTestCases uid is not listed in MOBILE_DATA_PREFERRED_UIDS setting, so the
@@ -3342,7 +3362,7 @@
         // Create test network agent with restricted network.
         final NetworkCapabilities nc = new NetworkCapabilities.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         TEST_RESTRICTED_NW_IFACE_NAME))
                 .build();
@@ -3376,23 +3396,23 @@
                 mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
 
         // File a restricted network request with permission first to hold the connection.
-        final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
         final NetworkRequest testRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         TEST_RESTRICTED_NW_IFACE_NAME))
                 .build();
-        runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
+        final TestableNetworkCallback testNetworkCb = runWithShellPermissionIdentity(
+                () -> networkCallbackRule.requestNetwork(testRequest),
                 CONNECTIVITY_USE_RESTRICTED_NETWORKS);
 
         // File another restricted network request without permission.
         final TestableNetworkCallback restrictedNetworkCb = new TestableNetworkCallback();
         final NetworkRequest restrictedRequest = new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
                 .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
                         TEST_RESTRICTED_NW_IFACE_NAME))
                 .build();
@@ -3409,7 +3429,7 @@
                     NETWORK_CALLBACK_TIMEOUT_MS,
                     entry -> network.equals(entry.getNetwork())
                             && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                            .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+                            .hasCapability(NET_CAPABILITY_NOT_RESTRICTED)));
             // CtsNetTestCases package doesn't hold CONNECTIVITY_USE_RESTRICTED_NETWORKS, so it
             // does not allow to bind socket to restricted network.
             assertThrows(IOException.class, () -> network.bindSocket(socket));
@@ -3427,13 +3447,13 @@
 
             if (TestUtils.shouldTestTApis()) {
                 // Uid is in allowed list. Try file network request again.
-                requestNetwork(restrictedRequest, restrictedNetworkCb);
+                networkCallbackRule.requestNetwork(restrictedRequest, restrictedNetworkCb);
                 // Verify that the network is restricted.
                 restrictedNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
                         NETWORK_CALLBACK_TIMEOUT_MS,
                         entry -> network.equals(entry.getNetwork())
                                 && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                                .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+                                .hasCapability(NET_CAPABILITY_NOT_RESTRICTED)));
             }
         } finally {
             agent.unregister();
@@ -3572,6 +3592,8 @@
         doTestFirewallBlocking(FIREWALL_CHAIN_DOZABLE, ALLOWLIST);
     }
 
+    // Disable test - needs to be fixed
+    @Ignore
     @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
     @AppModeFull(reason = "Socket cannot bind in instant app mode")
     public void testFirewallBlockingBackground() {
@@ -3726,63 +3748,263 @@
                 Process.myUid() + 1, EXPECT_OPEN);
     }
 
+    private int getBlockedReason(final int chain) {
+        switch(chain) {
+            case FIREWALL_CHAIN_DOZABLE:
+                return BLOCKED_REASON_DOZE;
+            case  FIREWALL_CHAIN_POWERSAVE:
+                return BLOCKED_REASON_BATTERY_SAVER;
+            case  FIREWALL_CHAIN_RESTRICTED:
+                return BLOCKED_REASON_RESTRICTED_MODE;
+            case  FIREWALL_CHAIN_LOW_POWER_STANDBY:
+                return BLOCKED_REASON_LOW_POWER_STANDBY;
+            case  FIREWALL_CHAIN_BACKGROUND:
+                return BLOCKED_REASON_APP_BACKGROUND;
+            case  FIREWALL_CHAIN_STANDBY:
+                return BLOCKED_REASON_APP_STANDBY;
+            case FIREWALL_CHAIN_METERED_DENY_USER:
+                return BLOCKED_METERED_REASON_USER_RESTRICTED;
+            case FIREWALL_CHAIN_METERED_DENY_ADMIN:
+                return BLOCKED_METERED_REASON_ADMIN_DISABLED;
+            case FIREWALL_CHAIN_OEM_DENY_1:
+            case FIREWALL_CHAIN_OEM_DENY_2:
+            case FIREWALL_CHAIN_OEM_DENY_3:
+                return BLOCKED_REASON_OEM_DENY;
+            default:
+                throw new IllegalArgumentException(
+                        "Failed to find blockedReasons for chain: " + chain);
+        }
+    }
+
+    private void doTestBlockedReasons_setUidFirewallRule(final int chain, final boolean metered)
+            throws Exception {
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
+
+        // Store current Wi-Fi metered value and update metered value
+        final Network currentWifiNetwork = mCtsNetUtils.ensureWifiConnected();
+        final NetworkCapabilities wifiNetworkCapabilities = callWithShellPermissionIdentity(
+                () -> mCm.getNetworkCapabilities(currentWifiNetwork));
+        final String ssid = unquoteSSID(wifiNetworkCapabilities.getSsid());
+        final boolean oldMeteredValue = wifiNetworkCapabilities.isMetered();
+        final Network wifiNetwork =
+                setWifiMeteredStatusAndWait(ssid, metered, true /* waitForValidation */);
+
+        // Store current firewall chains status. This test operates on the chain that is passed in,
+        // but also always operates on FIREWALL_CHAIN_METERED_DENY_USER to ensure that metered
+        // chains are tested as well.
+        final int myUid = Process.myUid();
+        final boolean wasChainEnabled = runWithShellPermissionIdentity(
+                () -> mCm.getFirewallChainEnabled(chain), NETWORK_SETTINGS);
+        final int previousFirewallRule = runWithShellPermissionIdentity(
+                () -> mCm.getUidFirewallRule(chain, myUid));
+        final int previousMeteredDenyFirewallRule = runWithShellPermissionIdentity(
+                () -> mCm.getUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, myUid));
+
+        final DetailedBlockedStatusCallback cb = new DetailedBlockedStatusCallback();
+        networkCallbackRule.requestNetwork(makeWifiNetworkRequest(), cb);
+        testAndCleanup(() -> {
+            int blockedReasonsWithoutChain = BLOCKED_REASON_NONE;
+            int blockedReasonsWithChain = getBlockedReason(chain);
+            int blockedReasonsWithChainAndLockDown =
+                    getBlockedReason(chain) | BLOCKED_REASON_LOCKDOWN_VPN;
+            if (metered) {
+                blockedReasonsWithoutChain |= BLOCKED_METERED_REASON_USER_RESTRICTED;
+                blockedReasonsWithChain |= BLOCKED_METERED_REASON_USER_RESTRICTED;
+                blockedReasonsWithChainAndLockDown |= BLOCKED_METERED_REASON_USER_RESTRICTED;
+            }
+
+            // Set RULE_DENY on target chain and metered deny chain
+            runWithShellPermissionIdentity(() -> {
+                mCm.setFirewallChainEnabled(chain, true /* enable */);
+                mCm.setUidFirewallRule(chain, myUid, FIREWALL_RULE_DENY);
+                mCm.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, myUid,
+                        FIREWALL_RULE_DENY);
+            }, NETWORK_SETTINGS);
+            cb.eventuallyExpectBlockedStatusCallback(wifiNetwork, blockedReasonsWithChain);
+
+            // Set VPN lockdown
+            final Range<Integer> myUidRange = new Range<>(myUid, myUid);
+            runWithShellPermissionIdentity(() -> setRequireVpnForUids(
+                    true /* requireVpn */, List.of(myUidRange)), NETWORK_SETTINGS);
+            cb.eventuallyExpectBlockedStatusCallback(wifiNetwork,
+                    blockedReasonsWithChainAndLockDown);
+
+            // Unset VPN lockdown
+            runWithShellPermissionIdentity(() -> setRequireVpnForUids(
+                    false /* requireVpn */, List.of(myUidRange)), NETWORK_SETTINGS);
+            cb.eventuallyExpectBlockedStatusCallback(wifiNetwork, blockedReasonsWithChain);
+
+            // Set RULE_ALLOW on target chain
+            runWithShellPermissionIdentity(
+                    () -> mCm.setUidFirewallRule(chain, myUid, FIREWALL_RULE_ALLOW),
+                    NETWORK_SETTINGS);
+            cb.eventuallyExpectBlockedStatusCallback(wifiNetwork, blockedReasonsWithoutChain);
+
+            // Set RULE_ALLOW on metered deny chain
+            runWithShellPermissionIdentity(() -> mCm.setUidFirewallRule(
+                            FIREWALL_CHAIN_METERED_DENY_USER, myUid, FIREWALL_RULE_ALLOW),
+                    NETWORK_SETTINGS);
+            if (metered) {
+                cb.eventuallyExpectBlockedStatusCallback(wifiNetwork, BLOCKED_REASON_NONE);
+            }
+        }, /* cleanup */ () -> {
+            setWifiMeteredStatusAndWait(ssid, oldMeteredValue, false /* waitForValidation */);
+        }, /* cleanup */ () -> {
+            mCm.unregisterNetworkCallback(cb);
+        }, /* cleanup */ () -> {
+            runWithShellPermissionIdentity(() -> {
+                mCm.setFirewallChainEnabled(chain, wasChainEnabled);
+                try {
+                    mCm.setUidFirewallRule(chain, myUid, previousFirewallRule);
+                } catch (IllegalStateException ignored) {
+                    // Removing match causes an exception when the rule entry for the uid does
+                    // not exist. But this is fine and can be ignored.
+                }
+                try {
+                    mCm.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, myUid,
+                            previousMeteredDenyFirewallRule);
+                } catch (IllegalStateException ignored) {
+                    // Removing match causes an exception when the rule entry for the uid does
+                    // not exist. But this is fine and can be ignored.
+                }
+            }, NETWORK_SETTINGS);
+        });
+    }
+
+    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+    public void testBlockedReasons_setUidFirewallRule() throws Exception {
+        doTestBlockedReasons_setUidFirewallRule(FIREWALL_CHAIN_DOZABLE, true /* metered */);
+        doTestBlockedReasons_setUidFirewallRule(FIREWALL_CHAIN_STANDBY, false /* metered */);
+    }
+
+    private void doTestBlockedReasons_setFirewallChainEnabled(final int chain) {
+        // Store current firewall chains status.
+        final int myUid = Process.myUid();
+        // TODO(b/342508466): Use runAsShell
+        final boolean wasChainEnabled = runWithShellPermissionIdentity(
+                () -> mCm.getFirewallChainEnabled(chain), NETWORK_SETTINGS);
+        final int previousFirewallRule = runWithShellPermissionIdentity(
+                () -> mCm.getUidFirewallRule(chain, myUid), NETWORK_SETTINGS);
+
+        final DetailedBlockedStatusCallback cb = new DetailedBlockedStatusCallback();
+        networkCallbackRule.registerDefaultNetworkCallback(cb);
+        final Network network = cb.expect(CallbackEntry.AVAILABLE).getNetwork();
+        testAndCleanup(() -> {
+            // Disable chain and set RULE_DENY on target chain
+            runWithShellPermissionIdentity(() -> {
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+                mCm.setUidFirewallRule(chain, myUid, FIREWALL_RULE_DENY);
+            }, NETWORK_SETTINGS);
+            cb.eventuallyExpectBlockedStatusCallback(network, BLOCKED_REASON_NONE);
+
+            // Enable chain
+            runWithShellPermissionIdentity(() -> {
+                mCm.setFirewallChainEnabled(chain, true /* enable */);
+            }, NETWORK_SETTINGS);
+            cb.eventuallyExpectBlockedStatusCallback(network, getBlockedReason(chain));
+
+            // Disable chain
+            runWithShellPermissionIdentity(() -> {
+                mCm.setFirewallChainEnabled(chain, false /* enable */);
+            }, NETWORK_SETTINGS);
+            cb.eventuallyExpectBlockedStatusCallback(network, BLOCKED_REASON_NONE);
+        }, /* cleanup */ () -> {
+            runWithShellPermissionIdentity(() -> {
+                mCm.setFirewallChainEnabled(chain, wasChainEnabled);
+                try {
+                    mCm.setUidFirewallRule(chain, myUid, previousFirewallRule);
+                } catch (IllegalStateException ignored) {
+                    // Removing match causes an exception when the rule entry for the uid does
+                    // not exist. But this is fine and can be ignored.
+                }
+            }, NETWORK_SETTINGS);
+        });
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+    public void testBlockedReasons_setFirewallChainEnabled() {
+        doTestBlockedReasons_setFirewallChainEnabled(FIREWALL_CHAIN_POWERSAVE);
+        doTestBlockedReasons_setFirewallChainEnabled(FIREWALL_CHAIN_OEM_DENY_1);
+    }
+
+    private void doTestBlockedReasons_replaceFirewallChain(
+            final int chain, final boolean isAllowList) {
+        // Store current firewall chains status.
+        final int myUid = Process.myUid();
+        final boolean wasChainEnabled = runWithShellPermissionIdentity(
+                () -> mCm.getFirewallChainEnabled(chain), NETWORK_SETTINGS);
+        final int previousFirewallRule = runWithShellPermissionIdentity(
+                () -> mCm.getUidFirewallRule(chain, myUid), NETWORK_SETTINGS);
+
+        final DetailedBlockedStatusCallback cb = new DetailedBlockedStatusCallback();
+        networkCallbackRule.registerDefaultNetworkCallback(cb);
+        final Network network = cb.expect(CallbackEntry.AVAILABLE).getNetwork();
+        testAndCleanup(() -> {
+            cb.eventuallyExpectBlockedStatusCallback(network, BLOCKED_REASON_NONE);
+
+            // Remove uid from the target chain and enable chain
+            runWithShellPermissionIdentity(() -> {
+                // Note that this removes *all* UIDs from the chain, not just the UID that is
+                // being tested. This is probably OK since FIREWALL_CHAIN_OEM_DENY_2 is unused
+                // in AOSP and FIREWALL_CHAIN_BACKGROUND is probably empty when running this
+                // test (since nothing is in the foreground).
+                //
+                // TODO(b/342508466): add a getFirewallUidChainContents or similar method to fetch
+                // chain contents, and update this test to use it.
+                mCm.replaceFirewallChain(chain, new int[0]);
+                mCm.setFirewallChainEnabled(chain, true /* enable */);
+            }, NETWORK_SETTINGS);
+
+            if (isAllowList) {
+                cb.eventuallyExpectBlockedStatusCallback(network, getBlockedReason(chain));
+            } else {
+                cb.assertNoBlockedStatusCallback();
+            }
+
+            // Put uid on the target chain
+            runWithShellPermissionIdentity(
+                    () -> mCm.replaceFirewallChain(chain, new int[]{myUid}), NETWORK_SETTINGS);
+
+            if (isAllowList) {
+                cb.eventuallyExpectBlockedStatusCallback(network, BLOCKED_REASON_NONE);
+            } else {
+                cb.eventuallyExpectBlockedStatusCallback(network, getBlockedReason(chain));
+            }
+
+            // Remove uid from the target chain
+            runWithShellPermissionIdentity(
+                    () -> mCm.replaceFirewallChain(chain, new int[0]), NETWORK_SETTINGS);
+
+            if (isAllowList) {
+                cb.eventuallyExpectBlockedStatusCallback(network, getBlockedReason(chain));
+            } else {
+                cb.eventuallyExpectBlockedStatusCallback(network, BLOCKED_REASON_NONE);
+            }
+        }, /* cleanup */ () -> {
+            runWithShellPermissionIdentity(() -> {
+                mCm.setFirewallChainEnabled(chain, wasChainEnabled);
+                try {
+                    mCm.setUidFirewallRule(chain, myUid, previousFirewallRule);
+                } catch (IllegalStateException ignored) {
+                    // Removing match causes an exception when the rule entry for the uid does
+                    // not exist. But this is fine and can be ignored.
+                }
+            }, NETWORK_SETTINGS);
+        });
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+    public void testBlockedReasons_replaceFirewallChain() {
+        doTestBlockedReasons_replaceFirewallChain(
+                FIREWALL_CHAIN_BACKGROUND, true /* isAllowChain */);
+        doTestBlockedReasons_replaceFirewallChain(
+                FIREWALL_CHAIN_OEM_DENY_2, false /* isAllowChain */);
+    }
+
     private void assumeTestSApis() {
         // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
         // shims, and @IgnoreUpTo does not check that.
         assumeTrue(TestUtils.shouldTestSApis());
     }
-
-    private void unregisterRegisteredCallbacks() {
-        for (NetworkCallback callback: mRegisteredCallbacks) {
-            mCm.unregisterNetworkCallback(callback);
-        }
-    }
-
-    private void registerDefaultNetworkCallback(NetworkCallback callback) {
-        mCm.registerDefaultNetworkCallback(callback);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerDefaultNetworkCallback(NetworkCallback callback, Handler handler) {
-        mCm.registerDefaultNetworkCallback(callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerNetworkCallback(NetworkRequest request, NetworkCallback callback) {
-        mCm.registerNetworkCallback(request, callback);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerSystemDefaultNetworkCallback(NetworkCallback callback, Handler handler) {
-        mCmShim.registerSystemDefaultNetworkCallback(callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerDefaultNetworkCallbackForUid(int uid, NetworkCallback callback,
-            Handler handler) throws Exception {
-        mCmShim.registerDefaultNetworkCallbackForUid(uid, callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void requestNetwork(NetworkRequest request, NetworkCallback callback) {
-        mCm.requestNetwork(request, callback);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void requestNetwork(NetworkRequest request, NetworkCallback callback, int timeoutSec) {
-        mCm.requestNetwork(request, callback, timeoutSec);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void registerBestMatchingNetworkCallback(NetworkRequest request,
-            NetworkCallback callback, Handler handler) {
-        mCm.registerBestMatchingNetworkCallback(request, callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
-
-    private void requestBackgroundNetwork(NetworkRequest request, NetworkCallback callback,
-            Handler handler) throws Exception {
-        mCmShim.requestBackgroundNetwork(request, callback, handler);
-        mRegisteredCallbacks.add(callback);
-    }
 }
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 752891f..fa44ae9 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -852,13 +852,14 @@
     }
 
     public void doTestContinuousQueries(Executor executor) throws InterruptedException {
-        final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN;
         for (Network network : getTestableNetworks()) {
             for (int i = 0; i < QUERY_TIMES ; ++i) {
-                final VerifyCancelInetAddressCallback callback =
-                        new VerifyCancelInetAddressCallback(msg, null);
                 // query v6/v4 in turn
                 boolean queryV6 = (i % 2 == 0);
+                final String msg = "Test continuous " + QUERY_TIMES + " queries " + TEST_DOMAIN
+                        + " on " + network + ", queryV6=" + queryV6;
+                final VerifyCancelInetAddressCallback callback =
+                        new VerifyCancelInetAddressCallback(msg, null);
                 mDns.query(network, TEST_DOMAIN, queryV6 ? TYPE_AAAA : TYPE_A,
                         FLAG_NO_CACHE_LOOKUP, executor, null, callback);
 
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index b7e5205..61ebd8f 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -303,18 +303,6 @@
         fun expectOnAvailable(timeout: Long = TIMEOUT_MS): String {
             return available.get(timeout, TimeUnit.MILLISECONDS)
         }
-
-        fun expectOnUnavailable() {
-            // Assert that the future fails with the IllegalStateException from the
-            // completeExceptionally() call inside onUnavailable.
-            assertFailsWith(IllegalStateException::class) {
-                try {
-                    available.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
-                } catch (e: ExecutionException) {
-                    throw e.cause!!
-                }
-            }
-        }
     }
 
     private class EthernetOutcomeReceiver :
@@ -348,7 +336,9 @@
         }
     }
 
-    private fun isEthernetSupported() = em != null
+    private fun isEthernetSupported() : Boolean {
+        return context.getSystemService(EthernetManager::class.java) != null
+    }
 
     @Before
     fun setUp() {
@@ -899,6 +889,24 @@
     }
 
     @Test
+    fun testEnableDisableInterface_callbacks() {
+        val iface = createInterface()
+        val listener = EthernetStateListener()
+        addInterfaceStateListener(listener)
+        // Uses eventuallyExpect to account for interfaces that could already exist on device
+        listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+        disableInterface(iface).expectResult(iface.name)
+        listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+
+        enableInterface(iface).expectResult(iface.name)
+        listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+        disableInterface(iface).expectResult(iface.name)
+        listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+    }
+
+    @Test
     fun testUpdateConfiguration_forBothIpConfigAndCapabilities() {
         val iface = createInterface()
         val cb = requestNetwork(ETH_REQUEST.copyWithEthernetSpecifier(iface.name))
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index 7f710d7..2a6c638 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -26,12 +26,15 @@
 import static android.system.OsConstants.FIONREAD;
 
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.IpSecAlgorithm;
 import android.net.IpSecManager;
 import android.net.IpSecTransform;
+import android.net.IpSecTransformState;
+import android.os.OutcomeReceiver;
 import android.platform.test.annotations.AppModeFull;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -65,8 +68,12 @@
 import java.net.SocketImpl;
 import java.net.SocketOptions;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 @RunWith(AndroidJUnit4.class)
@@ -83,6 +90,7 @@
     protected static final byte[] TEST_DATA = "Best test data ever!".getBytes();
     protected static final int DATA_BUFFER_LEN = 4096;
     protected static final int SOCK_TIMEOUT = 500;
+    protected static final int REPLAY_BITMAP_LEN_BYTE = 512;
 
     private static final byte[] KEY_DATA = {
         0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
@@ -122,6 +130,47 @@
                                 .getSystemService(Context.CONNECTIVITY_SERVICE);
     }
 
+    protected static void checkTransformState(
+            IpSecTransform transform,
+            long txHighestSeqNum,
+            long rxHighestSeqNum,
+            long packetCnt,
+            long byteCnt,
+            byte[] replayBitmap)
+            throws Exception {
+        final CompletableFuture<IpSecTransformState> futureIpSecTransform =
+                new CompletableFuture<>();
+        transform.requestIpSecTransformState(
+                Executors.newSingleThreadExecutor(),
+                new OutcomeReceiver<IpSecTransformState, RuntimeException>() {
+                    @Override
+                    public void onResult(IpSecTransformState state) {
+                        futureIpSecTransform.complete(state);
+                    }
+                });
+
+        final IpSecTransformState transformState =
+                futureIpSecTransform.get(SOCK_TIMEOUT, TimeUnit.MILLISECONDS);
+
+        assertEquals(txHighestSeqNum, transformState.getTxHighestSequenceNumber());
+        assertEquals(rxHighestSeqNum, transformState.getRxHighestSequenceNumber());
+        assertEquals(packetCnt, transformState.getPacketCount());
+        assertEquals(byteCnt, transformState.getByteCount());
+        assertArrayEquals(replayBitmap, transformState.getReplayBitmap());
+    }
+
+    protected static void checkTransformStateNoTraffic(IpSecTransform transform) throws Exception {
+        checkTransformState(transform, 0L, 0L, 0L, 0L, newReplayBitmap(0));
+    }
+
+    protected static byte[] newReplayBitmap(int receivedPktCnt) {
+        final BitSet bitSet = new BitSet(REPLAY_BITMAP_LEN_BYTE * 8);
+        for (int i = 0; i < receivedPktCnt; i++) {
+            bitSet.set(i);
+        }
+        return Arrays.copyOf(bitSet.toByteArray(), REPLAY_BITMAP_LEN_BYTE);
+    }
+
     /** Checks if an IPsec algorithm is enabled on the device */
     protected static boolean hasIpSecAlgorithm(String algorithm) {
         if (SdkLevel.isAtLeastS()) {
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index fe86a90..e94d94f 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -63,13 +63,17 @@
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
+import android.net.InetAddresses;
 import android.net.IpSecAlgorithm;
 import android.net.IpSecManager;
 import android.net.IpSecManager.SecurityParameterIndex;
 import android.net.IpSecManager.UdpEncapsulationSocket;
 import android.net.IpSecTransform;
+import android.net.IpSecTransformState;
+import android.net.NetworkUtils;
 import android.net.TrafficStats;
 import android.os.Build;
+import android.os.OutcomeReceiver;
 import android.platform.test.annotations.AppModeFull;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -79,6 +83,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.SkipMainlinePresubmit;
@@ -98,7 +103,11 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
+@ConnectivityModuleTest
 @RunWith(AndroidJUnit4.class)
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 public class IpSecManagerTest extends IpSecBaseTest {
@@ -381,6 +390,22 @@
         assumeTrue("Not supported by kernel", isIpv6UdpEncapSupportedByKernel());
     }
 
+    // TODO: b/319532485 Figure out whether to support x86_32
+    private static boolean isRequestTransformStateSupportedByKernel() {
+        return NetworkUtils.isKernel64Bit() || !NetworkUtils.isKernelX86();
+    }
+
+    // Package private for use in IpSecManagerTunnelTest
+    static boolean isRequestTransformStateSupported() {
+        return SdkLevel.isAtLeastV() && isRequestTransformStateSupportedByKernel();
+    }
+
+    // Package private for use in IpSecManagerTunnelTest
+    static void assumeRequestIpSecTransformStateSupported() {
+        assumeTrue("Not supported before V", SdkLevel.isAtLeastV());
+        assumeTrue("Not supported by kernel", isRequestTransformStateSupportedByKernel());
+    }
+
     @Test
     public void testCreateTransformIpv4() throws Exception {
         doTestCreateTransform(IPV4_LOOPBACK, false);
@@ -426,6 +451,12 @@
             long uidTxDelta = 0;
             long uidRxDelta = 0;
             for (int i = 0; i < 100; i++) {
+                // Since the target SDK of this test should always equal or be larger than V,
+                // TrafficStats caching is always enabled. Clearing the cache is needed to
+                // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+                if (SdkLevel.isAtLeastT()) {
+                    runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
+                }
                 uidTxDelta = TrafficStats.getUidTxPackets(Os.getuid()) - uidTxPackets;
                 uidRxDelta = TrafficStats.getUidRxPackets(Os.getuid()) - uidRxPackets;
 
@@ -500,6 +531,12 @@
         }
 
         private static void initStatsChecker() throws Exception {
+            // Since the target SDK of this test should always equal or be larger than V,
+            // TrafficStats caching is always enabled. Clearing the cache is needed to
+            // avoid rate-limiting on devices with a mainlined (T+) NetworkStatsService.
+            if (SdkLevel.isAtLeastT()) {
+                runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
+            }
             uidTxBytes = TrafficStats.getUidTxBytes(Os.getuid());
             uidRxBytes = TrafficStats.getUidRxBytes(Os.getuid());
             uidTxPackets = TrafficStats.getUidTxPackets(Os.getuid());
@@ -1596,4 +1633,65 @@
             assertTrue("Returned invalid port", encapSocket.getPort() != 0);
         }
     }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    public void testRequestIpSecTransformState() throws Exception {
+        assumeRequestIpSecTransformStateSupported();
+
+        final InetAddress localAddr = InetAddresses.parseNumericAddress(IPV6_LOOPBACK);
+        try (SecurityParameterIndex spi = mISM.allocateSecurityParameterIndex(localAddr);
+                IpSecTransform transform =
+                        buildTransportModeTransform(spi, localAddr, null /* encapSocket*/)) {
+            final SocketPair<JavaUdpSocket> sockets =
+                    getJavaUdpSocketPair(localAddr, mISM, transform, false);
+
+            sockets.mLeftSock.sendTo(TEST_DATA, localAddr, sockets.mRightSock.getPort());
+            sockets.mRightSock.receive();
+
+            final int expectedPacketCount = 1;
+            final int expectedInnerPacketSize = TEST_DATA.length + UDP_HDRLEN;
+
+            checkTransformState(
+                    transform,
+                    expectedPacketCount,
+                    expectedPacketCount,
+                    2 * (long) expectedPacketCount,
+                    2 * (long) expectedInnerPacketSize,
+                    newReplayBitmap(expectedPacketCount));
+        }
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    public void testRequestIpSecTransformStateOnClosedTransform() throws Exception {
+        assumeRequestIpSecTransformStateSupported();
+
+        final InetAddress localAddr = InetAddresses.parseNumericAddress(IPV6_LOOPBACK);
+        final CompletableFuture<RuntimeException> futureError = new CompletableFuture<>();
+
+        try (SecurityParameterIndex spi = mISM.allocateSecurityParameterIndex(localAddr);
+                IpSecTransform transform =
+                        buildTransportModeTransform(spi, localAddr, null /* encapSocket*/)) {
+            transform.close();
+
+            transform.requestIpSecTransformState(
+                    Executors.newSingleThreadExecutor(),
+                    new OutcomeReceiver<IpSecTransformState, RuntimeException>() {
+                        @Override
+                        public void onResult(IpSecTransformState state) {
+                            fail("Expect to fail but received a state");
+                        }
+
+                        @Override
+                        public void onError(RuntimeException error) {
+                            futureError.complete(error);
+                        }
+                    });
+
+            assertTrue(
+                    futureError.get(SOCK_TIMEOUT, TimeUnit.MILLISECONDS)
+                            instanceof IllegalStateException);
+        }
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
index 1ede5c1..22a51d6 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
@@ -19,7 +19,9 @@
 import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
 import static android.net.IpSecManager.UdpEncapsulationSocket;
 import static android.net.cts.IpSecManagerTest.assumeExperimentalIpv6UdpEncapSupported;
+import static android.net.cts.IpSecManagerTest.assumeRequestIpSecTransformStateSupported;
 import static android.net.cts.IpSecManagerTest.isIpv6UdpEncapSupported;
+import static android.net.cts.IpSecManagerTest.isRequestTransformStateSupported;
 import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE;
 import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
 import static android.net.cts.PacketUtils.BytePayload;
@@ -117,6 +119,8 @@
 
     private static final int TIMEOUT_MS = 500;
 
+    private static final int PACKET_COUNT = 5000;
+
     // Static state to reduce setup/teardown
     private static ConnectivityManager sCM;
     private static TestNetworkManager sTNM;
@@ -256,7 +260,7 @@
     }
 
     /* Test runnables for callbacks after IPsec tunnels are set up. */
-    private abstract class IpSecTunnelTestRunnable {
+    private interface IpSecTunnelTestRunnable {
         /**
          * Runs the test code, and returns the inner socket port, if any.
          *
@@ -282,8 +286,7 @@
                 throws Exception;
     }
 
-    private int getPacketSize(
-            int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) {
+    private static int getInnerPacketSize(int innerFamily, boolean transportInTunnelMode) {
         int expectedPacketSize = TEST_DATA.length + UDP_HDRLEN;
 
         // Inner Transport mode packet size
@@ -299,6 +302,13 @@
         // Inner IP Header
         expectedPacketSize += innerFamily == AF_INET ? IP4_HDRLEN : IP6_HDRLEN;
 
+        return expectedPacketSize;
+    }
+
+    private static int getPacketSize(
+            int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) {
+        int expectedPacketSize = getInnerPacketSize(innerFamily, transportInTunnelMode);
+
         // Tunnel mode transform size
         expectedPacketSize =
                 PacketUtils.calculateEspPacketSize(
@@ -401,6 +411,20 @@
                             spi, TEST_DATA, useEncap, expectedPacketSize);
                     socket.close();
 
+                    if (isRequestTransformStateSupported()) {
+                        final int innerPacketSize =
+                                getInnerPacketSize(innerFamily, transportInTunnelMode);
+
+                        checkTransformState(
+                                outTunnelTransform,
+                                seqNum,
+                                0L,
+                                seqNum,
+                                seqNum * (long) innerPacketSize,
+                                newReplayBitmap(0));
+                        checkTransformStateNoTraffic(inTunnelTransform);
+                    }
+
                     return innerSocketPort;
                 }
             };
@@ -524,6 +548,22 @@
 
                     socket.close();
 
+                    if (isRequestTransformStateSupported()) {
+                        final int innerFamily =
+                                localInner instanceof Inet4Address ? AF_INET : AF_INET6;
+                        final int innerPacketSize =
+                                getInnerPacketSize(innerFamily, transportInTunnelMode);
+
+                        checkTransformStateNoTraffic(outTunnelTransform);
+                        checkTransformState(
+                                inTunnelTransform,
+                                0L,
+                                seqNum,
+                                seqNum,
+                                seqNum * (long) innerPacketSize,
+                                newReplayBitmap(seqNum));
+                    }
+
                     return 0;
                 }
             };
@@ -1127,6 +1167,18 @@
         return innerSocketPort;
     }
 
+    private int buildTunnelNetworkAndRunTestsSimple(int spi, IpSecTunnelTestRunnable test)
+            throws Exception {
+        return buildTunnelNetworkAndRunTests(
+                LOCAL_INNER_6,
+                REMOTE_INNER_6,
+                LOCAL_OUTER_6,
+                REMOTE_OUTER_6,
+                spi,
+                null /* encapSocket */,
+                test);
+    }
+
     private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception {
         byte[] socketResponseBytes = socket.receive();
         assertArrayEquals(TEST_DATA, socketResponseBytes);
@@ -1691,4 +1743,101 @@
         assumeExperimentalIpv6UdpEncapSupported();
         doTestMigrateTunnelModeTransform(AF_INET6, AF_INET6, true, false);
     }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    public void testRequestIpSecTransformStateForRx() throws Exception {
+        assumeRequestIpSecTransformStateSupported();
+
+        final int spi = getRandomSpi(LOCAL_OUTER_6, REMOTE_OUTER_6);
+        buildTunnelNetworkAndRunTestsSimple(
+                spi,
+                (ipsecNetwork,
+                        tunnelIface,
+                        tunUtils,
+                        inTunnelTransform,
+                        outTunnelTransform,
+                        localOuter,
+                        remoteOuter,
+                        seqNum) -> {
+                    // Build a socket and send traffic
+                    final JavaUdpSocket socket = new JavaUdpSocket(LOCAL_INNER_6);
+                    ipsecNetwork.bindSocket(socket.mSocket);
+                    int innerSocketPort = socket.getPort();
+
+                    for (int i = 1; i < PACKET_COUNT + 1; i++) {
+                        byte[] pkt =
+                                getTunnelModePacket(
+                                        spi,
+                                        REMOTE_INNER_6,
+                                        LOCAL_INNER_6,
+                                        remoteOuter,
+                                        localOuter,
+                                        innerSocketPort,
+                                        0,
+                                        i);
+                        tunUtils.injectPacket(pkt);
+                        receiveAndValidatePacket(socket);
+                    }
+
+                    final int innerPacketSize = getInnerPacketSize(AF_INET6, false);
+                    checkTransformState(
+                            inTunnelTransform,
+                            0L,
+                            PACKET_COUNT,
+                            PACKET_COUNT,
+                            PACKET_COUNT * (long) innerPacketSize,
+                            newReplayBitmap(REPLAY_BITMAP_LEN_BYTE * 8));
+
+                    return innerSocketPort;
+                });
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    public void testRequestIpSecTransformStateForTx() throws Exception {
+        assumeRequestIpSecTransformStateSupported();
+
+        final int spi = getRandomSpi(LOCAL_OUTER_6, REMOTE_OUTER_6);
+        buildTunnelNetworkAndRunTestsSimple(
+                spi,
+                (ipsecNetwork,
+                        tunnelIface,
+                        tunUtils,
+                        inTunnelTransform,
+                        outTunnelTransform,
+                        localOuter,
+                        remoteOuter,
+                        seqNum) -> {
+                    // Build a socket and send traffic
+                    final JavaUdpSocket outSocket = new JavaUdpSocket(LOCAL_INNER_6);
+                    ipsecNetwork.bindSocket(outSocket.mSocket);
+                    int innerSocketPort = outSocket.getPort();
+
+                    int expectedPacketSize =
+                            getPacketSize(
+                                    AF_INET6,
+                                    AF_INET6,
+                                    false /* useEncap */,
+                                    false /* transportInTunnelMode */);
+
+                    for (int i = 0; i < PACKET_COUNT; i++) {
+                        outSocket.sendTo(TEST_DATA, REMOTE_INNER_6, innerSocketPort);
+                        tunUtils.awaitEspPacketNoPlaintext(
+                                spi, TEST_DATA, false /* useEncap */, expectedPacketSize);
+                    }
+
+                    final int innerPacketSize =
+                            getInnerPacketSize(AF_INET6, false /* transportInTunnelMode */);
+                    checkTransformState(
+                            outTunnelTransform,
+                            PACKET_COUNT,
+                            0L,
+                            PACKET_COUNT,
+                            PACKET_COUNT * (long) innerPacketSize,
+                            newReplayBitmap(0));
+
+                    return innerSocketPort;
+                });
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 7ab73c2..2c7d5c6 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -40,12 +40,12 @@
 import android.system.OsConstants;
 import android.util.ArraySet;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.DeviceConfigRule;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -53,11 +53,16 @@
 
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+@RunWith(DevSdkIgnoreRunner.class)
 public class MultinetworkApiTest {
-    @Rule
+    @Rule(order = 1)
     public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
 
+    @Rule(order = 2)
+    public final AutoReleaseNetworkCallbackRule
+            mNetworkCallbackRule = new AutoReleaseNetworkCallbackRule();
+
     static {
         System.loadLibrary("nativemultinetwork_jni");
     }
@@ -93,13 +98,6 @@
         mCtsNetUtils = new CtsNetUtils(mContext);
     }
 
-    @After
-    public void tearDown() {
-        if (mCtsNetUtils.cellConnectAttempted()) {
-            mCtsNetUtils.disconnectFromCell();
-        }
-    }
-
     @Test
     public void testGetaddrinfo() throws Exception {
         for (Network network : getTestableNetworks()) {
@@ -268,14 +266,22 @@
      * Get all testable Networks with internet capability.
      */
     private Set<Network> getTestableNetworks() throws InterruptedException {
+        // Calling requestNetwork() to request a cell or Wi-Fi network via CtsNetUtils or
+        // NetworkCallbackRule requires the CHANGE_NETWORK_STATE permission. This permission cannot
+        // be granted to instant apps. Therefore, return currently available testable networks
+        // directly in instant mode.
+        if (mContext.getApplicationInfo().isInstantApp()) {
+            return new ArraySet<>(mCtsNetUtils.getTestableNetworks());
+        }
+
         // Obtain cell and Wi-Fi through CtsNetUtils (which uses NetworkCallbacks), as they may have
         // just been reconnected by the test using NetworkCallbacks, so synchronous calls may not
         // yet return them (synchronous calls and callbacks should not be mixed for a given
         // Network).
         final Set<Network> testableNetworks = new ArraySet<>();
         if (mContext.getPackageManager().hasSystemFeature(FEATURE_TELEPHONY)) {
-            if (!mCtsNetUtils.cellConnectAttempted()) {
-                mRequestedCellNetwork = mCtsNetUtils.connectToCell();
+            if (mRequestedCellNetwork == null) {
+                mRequestedCellNetwork = mNetworkCallbackRule.requestCell();
             }
             assertNotNull("Cell network requested but not obtained", mRequestedCellNetwork);
             testableNetworks.add(mRequestedCellNetwork);
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index beb9274..60081d4 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -194,15 +194,16 @@
 // TODO : enable this in a Mainline update or in V.
 private const val SHOULD_CREATE_NETWORKS_IMMEDIATELY = false
 
-@RunWith(DevSdkIgnoreRunner::class)
-// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
-// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
-@IgnoreUpTo(Build.VERSION_CODES.R)
+@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
 // NetworkAgent is updated as part of the connectivity module, and running NetworkAgent tests in MTS
 // for modules other than Connectivity does not provide much value. Only run them in connectivity
 // module MTS, so the tests only need to cover the case of an updated NetworkAgent.
 @ConnectivityModuleTest
-@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
+@DevSdkIgnoreRunner.RestoreDefaultNetwork
+// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
+// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
 class NetworkAgentTest {
     private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
     private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 6ec4e62..ff10e1a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -19,19 +19,19 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
-import static com.android.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.fail;
@@ -66,6 +66,7 @@
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import org.junit.Rule;
@@ -511,30 +512,20 @@
         assertArrayEquals(netCapabilities, nr.getCapabilities());
     }
 
-    @Test @IgnoreUpTo(VANILLA_ICE_CREAM)
+    // Default capabilities and default forbidden capabilities must not be changed on U- because
+    // this could cause the system server crash when there is a module rollback (b/313030307)
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R) @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testDefaultCapabilities() {
         final NetworkRequest defaultNR = new NetworkRequest.Builder().build();
-        assertTrue(defaultNR.hasForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK));
-        assertFalse(defaultNR.hasCapability(NET_CAPABILITY_LOCAL_NETWORK));
+
+        assertEquals(4, defaultNR.getCapabilities().length);
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_TRUSTED));
         assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VPN));
+        assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VCN_MANAGED));
 
-        final NetworkCapabilities emptyNC =
-                NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
-        assertFalse(defaultNR.canBeSatisfiedBy(emptyNC));
-
-        // defaultNC represent the capabilities of a network agent, so they must not contain
-        // forbidden capabilities by default.
-        final NetworkCapabilities defaultNC = new NetworkCapabilities.Builder().build();
-        assertArrayEquals(new int[0], defaultNC.getForbiddenCapabilities());
-        // A default NR can be satisfied by default NC.
-        assertTrue(defaultNR.canBeSatisfiedBy(defaultNC));
-
-        // Conversely, network requests have forbidden capabilities by default to manage
-        // backward compatibility, so test that these forbidden capabilities are in place.
-        // Starting in V, NET_CAPABILITY_LOCAL_NETWORK is introduced but is not seen by
-        // default, thanks to a default forbidden capability in NetworkRequest.
-        defaultNC.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
-        assertFalse(defaultNR.canBeSatisfiedBy(defaultNC));
+        assertEquals(0, defaultNR.getForbiddenCapabilities().length);
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index 6a019b7..2315940 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -805,7 +805,7 @@
                 // harness, which is untagged, won't cause a failure.
                 long firstTotal = resultsWithTraffic.get(0).total;
                 for (QueryResult queryResult : resultsWithTraffic) {
-                    assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 12);
+                    assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 16);
                 }
 
                 // Expect to see no traffic when querying for any tag in tagsWithNoTraffic or any
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
index 621af23..f9acb66 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTest.kt
@@ -19,7 +19,6 @@
 import android.Manifest.permission.MANAGE_TEST_NETWORKS
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.content.Context
-import android.content.pm.PackageManager
 import android.net.ConnectivityManager
 import android.net.EthernetManager
 import android.net.InetAddresses
@@ -36,7 +35,6 @@
 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER
 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST
 import android.net.dhcp.DhcpRequestPacket
-import android.os.Build
 import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import androidx.test.platform.app.InstrumentationRegistry
@@ -44,7 +42,7 @@
 import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress
 import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address
 import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.AutoCloseTestInterfaceRule
 import com.android.testutils.DhcpClientPacketFilter
 import com.android.testutils.DhcpOptionFilter
 import com.android.testutils.RecorderCallback.CallbackEntry
@@ -79,10 +77,6 @@
 @AppModeFull(reason = "Instant apps cannot create test networks")
 @RunWith(AndroidJUnit4::class)
 class NetworkValidationTest {
-    @JvmField
-    @Rule
-    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) }
     private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
@@ -104,23 +98,24 @@
 
     private var testSkipped = false
 
+    @get:Rule
+    val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
     @Before
     fun setUp() {
         // This test requires using a tap interface as an ethernet interface.
-        val pm = context.getPackageManager()
-        testSkipped = !pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET) &&
-                context.getSystemService(EthernetManager::class.java) == null
+        testSkipped = context.getSystemService(EthernetManager::class.java) == null
         assumeFalse(testSkipped)
 
         // Register a request so the network does not get torn down
         cm.requestNetwork(ethRequest, ethRequestCb)
         runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) {
             eth.setIncludeTestInterfaces(true)
-            // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
-            // does not go out of scope, which would cause it to close the underlying FileDescriptor
-            // in its finalizer.
-            iface = tnm.createTapInterface()
         }
+        // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
+        // does not go out of scope, which would cause it to close the underlying FileDescriptor
+        // in its finalizer.
+        iface = testInterfaceRule.createTapInterface()
 
         handlerThread.start()
         reader = TapPacketReader(
@@ -147,8 +142,6 @@
         handlerThread.threadHandler.post { reader.stop() }
         handlerThread.quitSafely()
         handlerThread.join()
-
-        iface.fileDescriptor.close()
     }
 
     @Test
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index 1b1f367..24af42b 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -17,18 +17,22 @@
 
 import android.net.EthernetTetheringTestBase
 import android.net.LinkAddress
-import android.net.TestNetworkInterface
 import android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL
 import android.net.TetheringManager.TETHERING_ETHERNET
 import android.net.TetheringManager.TetheringRequest
+import android.net.cts.util.EthernetTestInterface
 import android.net.nsd.NsdManager
 import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
 import android.platform.test.annotations.AppModeFull
 import androidx.test.filters.SmallTest
+import com.android.testutils.AutoCloseTestInterfaceRule
 import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.TapPacketReader
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.pollForQuery
 import com.android.testutils.tryTest
 import java.util.Random
 import kotlin.test.assertEquals
@@ -36,9 +40,12 @@
 import org.junit.After
 import org.junit.Assume.assumeFalse
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
+private const val TAG = "NsdManagerDownstreamTetheringTest"
+
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @ConnectivityModuleTest
@@ -48,32 +55,41 @@
     private val nsdManager by lazy { context.getSystemService(NsdManager::class.java)!! }
     private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
 
+    private val handlerThread = HandlerThread("$TAG thread").apply { start() }
+    private val handler = Handler(handlerThread.looper)
+    private lateinit var downstreamIface: EthernetTestInterface
+    private var tetheringEventCallback: MyTetheringEventCallback? = null
+
+    @get:Rule
+    val testInterfaceRule = AutoCloseTestInterfaceRule(context)
+
     @Before
     override fun setUp() {
         super.setUp()
-        setIncludeTestInterfaces(true)
+        val iface = testInterfaceRule.createTapInterface()
+        downstreamIface = EthernetTestInterface(context, handler, iface)
     }
 
     @After
     override fun tearDown() {
+        if (::downstreamIface.isInitialized) {
+            downstreamIface.destroy()
+        }
+        handlerThread.quitSafely()
+        handlerThread.join()
+        maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         super.tearDown()
-        setIncludeTestInterfaces(false)
     }
 
     @Test
     fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
         assumeFalse(isInterfaceForTetheringAvailable())
 
-        var downstreamIface: TestNetworkInterface? = null
-        var tetheringEventCallback: MyTetheringEventCallback? = null
-        var downstreamReader: TapPacketReader? = null
-
         val discoveryRecord = NsdDiscoveryRecord()
 
         tryTest {
-            downstreamIface = createTestInterface()
-            val iface = tetheredInterface
-            assertEquals(iface, downstreamIface?.interfaceName)
+            val iface = mTetheredInterfaceRequester.getInterface()
+            assertEquals(downstreamIface.name, iface)
             val request = TetheringRequest.Builder(TETHERING_ETHERNET)
                 .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
             tetheringEventCallback = enableEthernetTethering(
@@ -82,41 +98,27 @@
             ).apply {
                 awaitInterfaceLocalOnly()
             }
-            // This shouldn't be flaky because the TAP interface will buffer all packets even
-            // before the reader is started.
-            downstreamReader = makePacketReader(downstreamIface)
+            val downstreamReader = downstreamIface.packetReader
             waitForRouterAdvertisement(downstreamReader, iface, WAIT_RA_TIMEOUT_MS)
 
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
-            assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+            assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
-        } cleanupStep {
-            maybeStopTapPacketReader(downstreamReader)
-        } cleanupStep {
-            maybeCloseTestInterface(downstreamIface)
-        } cleanup {
-            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         }
     }
 
     @Test
     fun testMdnsDiscoveryWorkOnTetheringInterface() {
         assumeFalse(isInterfaceForTetheringAvailable())
-        setIncludeTestInterfaces(true)
-
-        var downstreamIface: TestNetworkInterface? = null
-        var tetheringEventCallback: MyTetheringEventCallback? = null
-        var downstreamReader: TapPacketReader? = null
 
         val discoveryRecord = NsdDiscoveryRecord()
 
         tryTest {
-            downstreamIface = createTestInterface()
-            val iface = tetheredInterface
-            assertEquals(iface, downstreamIface?.interfaceName)
+            val iface = mTetheredInterfaceRequester.getInterface()
+            assertEquals(downstreamIface.name, iface)
 
             val localAddr = LinkAddress("192.0.2.3/28")
             val clientAddr = LinkAddress("192.0.2.2/28")
@@ -130,23 +132,14 @@
                 awaitInterfaceTethered()
             }
 
-            val fd = downstreamIface?.fileDescriptor?.fileDescriptor
-            assertNotNull(fd)
-            downstreamReader = makePacketReader(fd, getMTU(downstreamIface))
-
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted>()
-            assertNotNull(downstreamReader?.pollForQuery("$serviceType.local", 12 /* type PTR */))
+            val downstreamReader = downstreamIface.packetReader
+            assertNotNull(downstreamReader.pollForQuery("$serviceType.local", 12 /* type PTR */))
             // TODO: Add another test to check packet reply can trigger serviceFound.
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord)
             discoveryRecord.expectCallback<NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped>()
-        } cleanupStep {
-            maybeStopTapPacketReader(downstreamReader)
-        } cleanupStep {
-            maybeCloseTestInterface(downstreamIface)
-        } cleanup {
-            maybeUnregisterTetheringEventCallback(tetheringEventCallback)
         }
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index ce2c2c1..c71d925 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -39,19 +39,6 @@
 import android.net.TestNetworkManager
 import android.net.TestNetworkSpecifier
 import android.net.connectivity.ConnectivityCompatChanges
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
-import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
-import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
-import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
-import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
-import android.net.cts.NsdResolveRecord.ResolveEvent.ServiceResolved
-import android.net.cts.NsdResolveRecord.ResolveEvent.StopResolutionFailed
-import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
-import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
-import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
 import android.net.cts.util.CtsNetUtils
 import android.net.nsd.DiscoveryRequest
 import android.net.nsd.NsdManager
@@ -78,9 +65,12 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.PollingCheck
 import com.android.compatibility.common.util.PropertyUtil
+import com.android.compatibility.common.util.SystemUtil
 import com.android.modules.utils.build.SdkLevel.isAtLeastU
 import com.android.net.module.util.DnsPacket
+import com.android.net.module.util.DnsPacket.ANSECTION
 import com.android.net.module.util.HexDump
+import com.android.net.module.util.HexDump.hexStringToByteArray
 import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
 import com.android.net.module.util.PacketBuilder
 import com.android.testutils.ConnectivityModuleTest
@@ -89,15 +79,40 @@
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.DeviceConfigRule
 import com.android.testutils.NSResponder
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import com.android.testutils.NsdEvent
+import com.android.testutils.NsdRecord
+import com.android.testutils.NsdRegistrationRecord
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import com.android.testutils.NsdResolveRecord
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ResolutionStopped
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ServiceResolved
+import com.android.testutils.NsdResolveRecord.ResolveEvent.StopResolutionFailed
+import com.android.testutils.NsdServiceInfoCallbackRecord
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.TapPacketReader
+import com.android.testutils.TestDnsPacket
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.assertEmpty
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
+import com.android.testutils.pollForAdvertisement
+import com.android.testutils.pollForMdnsPacket
+import com.android.testutils.pollForProbe
+import com.android.testutils.pollForQuery
+import com.android.testutils.pollForReply
 import com.android.testutils.runAsShell
 import com.android.testutils.tryTest
 import com.android.testutils.waitForIdle
@@ -112,8 +127,10 @@
 import java.util.Random
 import java.util.concurrent.Executor
 import kotlin.math.min
+import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import kotlin.test.assertNotEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.fail
@@ -129,6 +146,7 @@
 
 private const val TAG = "NsdManagerTest"
 private const val TIMEOUT_MS = 2000L
+
 // Registration may take a long time if there are devices with the same hostname on the network,
 // as the device needs to try another name and probe again. This is especially true since when using
 // mdnsresponder the usual hostname is "Android", and on conflict "Android-2", "Android-3", ... are
@@ -137,6 +155,9 @@
 private const val DBG = false
 private const val TEST_PORT = 12345
 private const val MDNS_PORT = 5353.toShort()
+private const val TYPE_KEY = 25
+private const val QCLASS_INTERNET = 0x0001
+private const val NAME_RECORDS_TTL_MILLIS: Long = 120
 private val multicastIpv6Addr = parseNumericAddress("ff02::fb") as Inet6Address
 private val testSrcAddr = parseNumericAddress("2001:db8::123") as Inet6Address
 
@@ -166,6 +187,13 @@
     private val serviceType2 = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
     private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
     private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
+    private val publicKey = hexStringToByteArray(
+            "0201030dc141d0637960b98cbc12cfca" +
+                    "221d2879dac26ee5b460e9007c992e19" +
+                    "02d897c391b03764d448f7d0c772fdb0" +
+                    "3b1d9d6d52ff8886769e8e2362513565" +
+                    "270962d3"
+    )
     private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
     private val ctsNetUtils by lazy{ CtsNetUtils(context) }
 
@@ -217,11 +245,14 @@
         val iface = tnm.createTapInterface()
         val cb = TestableNetworkCallback()
         val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
-        cm.requestNetwork(NetworkRequest.Builder()
+        cm.requestNetwork(
+            NetworkRequest.Builder()
                 .removeCapability(NET_CAPABILITY_TRUSTED)
                 .addTransportType(TRANSPORT_TEST)
                 .setNetworkSpecifier(testNetworkSpecifier)
-                .build(), cb)
+                .build(),
+            cb
+        )
         val agent = registerTestNetworkAgent(iface.interfaceName)
         val network = agent.network ?: fail("Registered agent should have a network")
 
@@ -241,12 +272,17 @@
         val lp = LinkProperties().apply {
             interfaceName = ifaceName
         }
-        val agent = TestableNetworkAgent(context, handlerThread.looper,
+        val agent = TestableNetworkAgent(
+            context,
+            handlerThread.looper,
                 NetworkCapabilities().apply {
                     removeCapability(NET_CAPABILITY_TRUSTED)
                     addTransportType(TRANSPORT_TEST)
                     setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
-                }, lp, NetworkAgentConfig.Builder().build())
+                },
+            lp,
+            NetworkAgentConfig.Builder().build()
+        )
         val network = agent.register()
         agent.markConnected()
         agent.expectCallback<OnNetworkCreated>()
@@ -320,15 +356,19 @@
                 Triple(null, null, "null key"),
                 Triple("", null, "empty key"),
                 Triple(string256, null, "key with 256 characters"),
-                Triple("key", string256.substring(3),
-                        "key+value combination with more than 255 characters"),
+                Triple(
+                    "key",
+                    string256.substring(3),
+                        "key+value combination with more than 255 characters"
+                ),
                 Triple("key", string256.substring(4), "key+value combination with 255 characters"),
                 Triple("\u0019", null, "key with invalid character"),
                 Triple("=", null, "key with invalid character"),
                 Triple("\u007f", null, "key with invalid character")
         ).forEach {
             assertFailsWith<IllegalArgumentException>(
-                    "Setting invalid ${it.third} unexpectedly succeeded") {
+                    "Setting invalid ${it.third} unexpectedly succeeded"
+            ) {
                 si.setAttribute(it.first, it.second)
             }
         }
@@ -351,7 +391,8 @@
         // Test registering without an Executor
         nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
         val registeredInfo = registrationRecord.expectCallback<ServiceRegistered>(
-                REGISTRATION_TIMEOUT_MS).serviceInfo
+                REGISTRATION_TIMEOUT_MS
+        ).serviceInfo
 
         // Only service name is included in ServiceRegistered callbacks
         assertNull(registeredInfo.serviceType)
@@ -366,7 +407,9 @@
 
         // Expect a service record to be discovered
         val foundInfo = discoveryRecord.waitForServiceDiscovered(
-                registeredInfo.serviceName, serviceType)
+                registeredInfo.serviceName,
+            serviceType
+        )
 
         // Test resolving without an Executor
         val resolveRecord = NsdResolveRecord()
@@ -381,8 +424,10 @@
         assertNull(resolvedService.attributes["booleanAttr"])
         assertEquals("value", resolvedService.attributes["keyValueAttr"].utf8ToString())
         assertEquals("=", resolvedService.attributes["keyEqualsAttr"].utf8ToString())
-        assertEquals(" value ",
-                resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString())
+        assertEquals(
+            " value ",
+                resolvedService.attributes[" whiteSpaceKeyValueAttr "].utf8ToString()
+        )
         assertEquals(string256.substring(9), resolvedService.attributes["longkey"].utf8ToString())
         assertArrayEquals(testByteArray, resolvedService.attributes["binaryDataAttr"])
         assertTrue(resolvedService.attributes.containsKey("nullBinaryDataAttr"))
@@ -416,12 +461,15 @@
         val registrationRecord2 = NsdRegistrationRecord()
         nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
         val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>(
-                REGISTRATION_TIMEOUT_MS).serviceInfo
+                REGISTRATION_TIMEOUT_MS
+        ).serviceInfo
 
         // Expect a service record to be discovered (and filter the ones
         // that are unrelated to this test)
         val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
-                registeredInfo2.serviceName, serviceType)
+                registeredInfo2.serviceName,
+            serviceType
+        )
 
         // Resolve the service
         val resolveRecord2 = NsdResolveRecord()
@@ -924,7 +972,8 @@
         // when the compat change is disabled.
         // Note that before T the compat constant had a different int value.
         assertFalse(CompatChanges.isChangeEnabled(
-                ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER))
+                ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER
+        ))
     }
 
     @Test
@@ -997,7 +1046,9 @@
         // onStopResolutionFailed on the record directly then verify it is received.
         val resolveRecord = NsdResolveRecord()
         resolveRecord.onStopResolutionFailed(
-                NsdServiceInfo(), NsdManager.FAILURE_OPERATION_NOT_RUNNING)
+                NsdServiceInfo(),
+            NsdManager.FAILURE_OPERATION_NOT_RUNNING
+        )
         val failedCb = resolveRecord.expectCallback<StopResolutionFailed>()
         assertEquals(NsdManager.FAILURE_OPERATION_NOT_RUNNING, failedCb.errorCode)
     }
@@ -1248,15 +1299,22 @@
 
         val si = makeTestServiceInfo(testNetwork1.network)
 
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
-                registrationRecord)
+        nsdManager.registerService(
+            si,
+            NsdManager.PROTOCOL_DNS_SD,
+            { it.run() },
+                registrationRecord
+        )
 
         tryTest {
             assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1287,15 +1345,22 @@
                     parseNumericAddress("2001:db8::3"))
         }
 
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
-                registrationRecord)
+        nsdManager.registerService(
+            si,
+            NsdManager.PROTOCOL_DNS_SD,
+            { it.run() },
+                registrationRecord
+        )
 
         tryTest {
             assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1326,15 +1391,22 @@
             hostname = customHostname
         }
 
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
         // Register service on testNetwork1
         val registrationRecord = NsdRegistrationRecord()
-        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
-                registrationRecord)
+        nsdManager.registerService(
+            si,
+            NsdManager.PROTOCOL_DNS_SD,
+            { it.run() },
+                registrationRecord
+        )
 
         tryTest {
             assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
@@ -1366,8 +1438,11 @@
         val registrationRecord = NsdRegistrationRecord()
         val discoveryRecord = NsdDiscoveryRecord()
         val registeredService = registerService(registrationRecord, si)
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -1445,15 +1520,15 @@
         val registeredService = registerService(registrationRecord, si)
         val packetReader = TapPacketReader(
                 Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
         tryTest {
-            repeat(3) {
-                assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
-                        "Expect 3 announcements sent after initial probing")
-            }
+            assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+                "No announcements sent after initial probing")
 
             assertEquals(si.serviceName, registeredService.serviceName)
             assertEquals(si.hostname, registeredService.hostname)
@@ -1477,11 +1552,11 @@
             val newRegistration =
                     registrationRecord
                             .expectCallbackEventually<ServiceRegistered>(REGISTRATION_TIMEOUT_MS) {
-                                it.serviceInfo.serviceName == serviceName
-                                        && it.serviceInfo.hostname.let { hostname ->
-                                    hostname != null
-                                            && hostname.startsWith(customHostname)
-                                            && hostname != customHostname
+                                it.serviceInfo.serviceName == serviceName &&
+                                        it.serviceInfo.hostname.let { hostname ->
+                                    hostname != null &&
+                                            hostname.startsWith(customHostname) &&
+                                            hostname != customHostname
                                 }
                             }
 
@@ -1512,8 +1587,11 @@
         val registrationRecord = NsdRegistrationRecord()
         val discoveryRecord = NsdDiscoveryRecord()
         val registeredService = registerService(registrationRecord, si)
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
@@ -1552,13 +1630,21 @@
     fun testDiscoveryWithPtrOnlyResponse_ServiceIsFound() {
         // Register service on testNetwork1
         val discoveryRecord = NsdDiscoveryRecord()
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
 
-        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
-                testNetwork1.network, { it.run() }, discoveryRecord)
+        nsdManager.discoverServices(
+            serviceType,
+            NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network,
+            { it.run() },
+            discoveryRecord
+        )
 
         tryTest {
             discoveryRecord.expectCallback<DiscoveryStarted>()
@@ -1602,8 +1688,10 @@
     fun testResolveWhenServerSendsNoAdditionalRecord() {
         // Resolve service on testNetwork1
         val resolveRecord = NsdResolveRecord()
-        val packetReader = TapPacketReader(Handler(handlerThread.looper),
-                testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
         )
         packetReader.startAsyncForTest()
         handlerThread.waitForIdle(TIMEOUT_MS)
@@ -1627,16 +1715,21 @@
                 rdata='testkey=testvalue')
         ))).hex()
          */
-        val srvTxtResponsePayload = HexDump.hexStringToByteArray("000084000000000200000000104" +
+        val srvTxtResponsePayload = HexDump.hexStringToByteArray(
+            "000084000000000200000000104" +
                 "e7364546573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f6" +
                 "3616c0000218001000000780011000000007a020874657374686f7374c030c00c00100001000" +
-                "00078001211746573746b65793d7465737476616c7565")
+                "00078001211746573746b65793d7465737476616c7565"
+        )
         replaceServiceNameAndTypeWithTestSuffix(srvTxtResponsePayload)
         packetReader.sendResponse(buildMdnsPacket(srvTxtResponsePayload))
 
         val testHostname = "testhost.local"
-        val addressQuery = packetReader.pollForQuery(testHostname,
-            DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA)
+        val addressQuery = packetReader.pollForQuery(
+            testHostname,
+            DnsResolver.TYPE_A,
+            DnsResolver.TYPE_AAAA
+        )
         assertNotNull(addressQuery)
 
         /*
@@ -1648,9 +1741,11 @@
                 rdata='2001:db8::123')
         ))).hex()
          */
-        val addressPayload = HexDump.hexStringToByteArray("0000840000000002000000000874657374" +
+        val addressPayload = HexDump.hexStringToByteArray(
+            "0000840000000002000000000874657374" +
                 "686f7374056c6f63616c0000010001000000780004c000027bc00c001c000100000078001020" +
-                "010db8000000000000000000000123")
+                "010db8000000000000000000000123"
+        )
         packetReader.sendResponse(buildMdnsPacket(addressPayload))
 
         val serviceResolved = resolveRecord.expectCallback<ServiceResolved>()
@@ -1664,7 +1759,8 @@
         }
         assertEquals(
                 setOf(parseNumericAddress("192.0.2.123"), parseNumericAddress("2001:db8::123")),
-                serviceResolved.serviceInfo.hostAddresses.toSet())
+                serviceResolved.serviceInfo.hostAddresses.toSet()
+        )
     }
 
     @Test
@@ -1691,7 +1787,7 @@
             scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
                     scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR', qclass=0x8001)
             )).hex()
-            */
+             */
             val mdnsPayload = HexDump.hexStringToByteArray("0000000000010000000000000d5f6e6d74313" +
                     "233343536373839045f746370056c6f63616c00000c8001")
             replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
@@ -1747,7 +1843,7 @@
                     an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
                             rdata='NsdTest123456789._nmt123456789._tcp.local')
             )).hex()
-            */
+             */
             val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
                     "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
                     "d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
@@ -1782,7 +1878,7 @@
                     an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
                             rdata='NsdTest123456789._nmt123456789._tcp.local')
             )).hex()
-            */
+             */
             val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
                     "3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
                     "0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
@@ -1834,7 +1930,7 @@
                     scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
                             qclass=0x8001)
             )).hex()
-            */
+             */
             val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
                     "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
                     "d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
@@ -1846,7 +1942,7 @@
                     an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
                             rdata='NsdTest._test._tcp.local')
             )).hex()
-            */
+             */
             val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
                     "045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
                     "746370056c6f63616c00")
@@ -1858,7 +1954,7 @@
                     an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
                             rdata='NsdTest123456789._nmt123456789._tcp.local')
             )).hex()
-            */
+             */
             val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
                     "3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
                     "3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
@@ -1887,6 +1983,72 @@
         }
     }
 
+    @Test
+    fun testQueryWhenKnownAnswerSuppressionFlagSet() {
+        // The flag may be removed in the future but known-answer suppression should be enabled by
+        // default in that case. The rule will reset flags automatically on teardown.
+        deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_query_with_known_answer", "1")
+
+        // Register service on testNetwork1
+        val discoveryRecord = NsdDiscoveryRecord()
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        nsdManager.discoverServices(
+            serviceType,
+            NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network,
+            { it.run() },
+            discoveryRecord
+        )
+
+        tryTest {
+            discoveryRecord.expectCallback<DiscoveryStarted>()
+            assertNotNull(packetReader.pollForQuery("$serviceType.local", DnsResolver.TYPE_PTR))
+            /*
+            Generated with:
+            scapy.raw(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+                scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=120,
+                rdata='NsdTest123456789._nmt123456789._tcp.local'))).hex()
+             */
+            val ptrResponsePayload = HexDump.hexStringToByteArray("0000840000000001000000000d5f6e" +
+                    "6d74313233343536373839045f746370056c6f63616c00000c000100000078002b104e736454" +
+                    "6573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+
+            replaceServiceNameAndTypeWithTestSuffix(ptrResponsePayload)
+            packetReader.sendResponse(buildMdnsPacket(ptrResponsePayload))
+
+            val serviceFound = discoveryRecord.expectCallback<ServiceFound>()
+            serviceFound.serviceInfo.let {
+                assertEquals(serviceName, it.serviceName)
+                // Discovered service types have a dot at the end
+                assertEquals("$serviceType.", it.serviceType)
+                assertEquals(testNetwork1.network, it.network)
+                // ServiceFound does not provide port, address or attributes (only information
+                // available in the PTR record is included in that callback, regardless of whether
+                // other records exist).
+                assertEquals(0, it.port)
+                assertEmpty(it.hostAddresses)
+                assertEquals(0, it.attributes.size)
+            }
+
+            // Expect the second query with a known answer
+            val query = packetReader.pollForMdnsPacket { pkt ->
+                pkt.isQueryFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+                        pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR)
+            }
+            assertNotNull(query)
+        } cleanup {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        }
+    }
+
     private fun makeLinkLocalAddressOfOtherDeviceOnPrefix(network: Network): Inet6Address {
         val lp = cm.getLinkProperties(network) ?: fail("No LinkProperties for net $network")
         // Expect to have a /64 link-local address
@@ -1905,10 +2067,12 @@
         val hostAddresses1 = listOf(
                 parseNumericAddress("192.0.2.23"),
                 parseNumericAddress("2001:db8::1"),
-                parseNumericAddress("2001:db8::2"))
+                parseNumericAddress("2001:db8::2")
+        )
         val hostAddresses2 = listOf(
                 parseNumericAddress("192.0.2.24"),
-                parseNumericAddress("2001:db8::3"))
+                parseNumericAddress("2001:db8::3")
+        )
         val si1 = NsdServiceInfo().also {
             it.network = testNetwork1.network
             it.serviceName = serviceName
@@ -1968,17 +2132,16 @@
     }
 
     @Test
-    fun testAdvertisingAndDiscovery_multipleRegistrationsForSameCustomHost_unionOfAddressesFound() {
+    fun testAdvertisingAndDiscovery_multipleRegistrationsForSameCustomHost_hostRenamed() {
         val hostAddresses1 = listOf(
                 parseNumericAddress("192.0.2.23"),
                 parseNumericAddress("2001:db8::1"),
-                parseNumericAddress("2001:db8::2"))
+                parseNumericAddress("2001:db8::2")
+        )
         val hostAddresses2 = listOf(
                 parseNumericAddress("192.0.2.24"),
-                parseNumericAddress("2001:db8::3"))
-        val hostAddresses3 = listOf(
-                parseNumericAddress("2001:db8::3"),
-                parseNumericAddress("2001:db8::5"))
+                parseNumericAddress("2001:db8::3")
+        )
         val si1 = NsdServiceInfo().also {
             it.network = testNetwork1.network
             it.hostname = customHostname
@@ -1992,18 +2155,9 @@
             it.hostname = customHostname
             it.hostAddresses = hostAddresses2
         }
-        val si3 = NsdServiceInfo().also {
-            it.network = testNetwork1.network
-            it.serviceName = serviceName3
-            it.serviceType = serviceType
-            it.port = TEST_PORT + 1
-            it.hostname = customHostname
-            it.hostAddresses = hostAddresses3
-        }
 
         val registrationRecord1 = NsdRegistrationRecord()
         val registrationRecord2 = NsdRegistrationRecord()
-        val registrationRecord3 = NsdRegistrationRecord()
 
         val discoveryRecord = NsdDiscoveryRecord()
         tryTest {
@@ -2013,27 +2167,13 @@
             nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
                     testNetwork1.network, Executor { it.run() }, discoveryRecord)
 
-            val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
+            val discoveredInfo = discoveryRecord.waitForServiceDiscovered(
                     serviceName, serviceType, testNetwork1.network)
-            val resolvedInfo1 = resolveService(discoveredInfo1)
+            val resolvedInfo = resolveService(discoveredInfo)
 
-            assertEquals(TEST_PORT, resolvedInfo1.port)
-            assertEquals(si1.hostname, resolvedInfo1.hostname)
-            assertAddressEquals(
-                    hostAddresses1 + hostAddresses2,
-                    resolvedInfo1.hostAddresses)
-
-            registerService(registrationRecord3, si3)
-
-            val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
-                    serviceName3, serviceType, testNetwork1.network)
-            val resolvedInfo2 = resolveService(discoveredInfo2)
-
-            assertEquals(TEST_PORT + 1, resolvedInfo2.port)
-            assertEquals(si2.hostname, resolvedInfo2.hostname)
-            assertAddressEquals(
-                    hostAddresses1 + hostAddresses2 + hostAddresses3,
-                    resolvedInfo2.hostAddresses)
+            assertEquals(TEST_PORT, resolvedInfo.port)
+            assertNotEquals(si1.hostname, resolvedInfo.hostname)
+            assertAddressEquals(hostAddresses2, resolvedInfo.hostAddresses)
         } cleanupStep {
             nsdManager.stopServiceDiscovery(discoveryRecord)
 
@@ -2041,7 +2181,6 @@
         } cleanup {
             nsdManager.unregisterService(registrationRecord1)
             nsdManager.unregisterService(registrationRecord2)
-            nsdManager.unregisterService(registrationRecord3)
         }
     }
 
@@ -2050,7 +2189,8 @@
         val hostAddresses = listOf(
                 parseNumericAddress("192.0.2.23"),
                 parseNumericAddress("2001:db8::1"),
-                parseNumericAddress("2001:db8::2"))
+                parseNumericAddress("2001:db8::2")
+        )
         val si1 = NsdServiceInfo().also {
             it.network = testNetwork1.network
             it.serviceType = serviceType
@@ -2106,6 +2246,405 @@
         }
     }
 
+    @Test
+    fun testRegisterService_registerImmediatelyAfterUnregister_serviceFound() {
+        val info1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+            serviceName = "service11111"
+            port = 11111
+        }
+        val info2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+            serviceName = "service22222"
+            port = 22222
+        }
+        val registrationRecord1 = NsdRegistrationRecord()
+        val discoveryRecord1 = NsdDiscoveryRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord1, info1)
+            nsdManager.discoverServices(serviceType,
+                    NsdManager.PROTOCOL_DNS_SD, testNetwork1.network, { it.run() },
+                    discoveryRecord1)
+            discoveryRecord1.waitForServiceDiscovered(info1.serviceName,
+                    serviceType, testNetwork1.network)
+            nsdManager.stopServiceDiscovery(discoveryRecord1)
+
+            nsdManager.unregisterService(registrationRecord1)
+            registerService(registrationRecord2, info2)
+            nsdManager.discoverServices(serviceType,
+                    NsdManager.PROTOCOL_DNS_SD, testNetwork1.network, { it.run() },
+                    discoveryRecord2)
+            val infoDiscovered = discoveryRecord2.waitForServiceDiscovered(info2.serviceName,
+                    serviceType, testNetwork1.network)
+            val infoResolved = resolveService(infoDiscovered)
+            assertEquals(22222, infoResolved.port)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+            discoveryRecord2.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord2)
+        }
+    }
+
+    @Test
+    fun testAdvertisingAndDiscovery_reregisterCustomHostWithDifferentAddresses_newAddressesFound() {
+        val si1 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.hostname = customHostname
+            it.hostAddresses = listOf(
+                    parseNumericAddress("192.0.2.23"),
+                    parseNumericAddress("2001:db8::1"))
+        }
+        val si2 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceName = serviceName
+            it.serviceType = serviceType
+            it.hostname = customHostname
+            it.port = TEST_PORT
+        }
+        val si3 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.hostname = customHostname
+            it.hostAddresses = listOf(
+                    parseNumericAddress("192.0.2.24"),
+                    parseNumericAddress("2001:db8::2"))
+        }
+
+        val registrationRecord1 = NsdRegistrationRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+        val registrationRecord3 = NsdRegistrationRecord()
+
+        val discoveryRecord = NsdDiscoveryRecord()
+
+        tryTest {
+            registerService(registrationRecord1, si1)
+            registerService(registrationRecord2, si2)
+
+            nsdManager.unregisterService(registrationRecord1)
+            registrationRecord1.expectCallback<ServiceUnregistered>()
+
+            registerService(registrationRecord3, si3)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, discoveryRecord)
+            val discoveredInfo = discoveryRecord.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            val resolvedInfo = resolveService(discoveredInfo)
+
+            assertEquals(serviceName, discoveredInfo.serviceName)
+            assertEquals(TEST_PORT, resolvedInfo.port)
+            assertEquals(customHostname, resolvedInfo.hostname)
+            assertAddressEquals(
+                    listOf(parseNumericAddress("192.0.2.24"), parseNumericAddress("2001:db8::2")),
+                    resolvedInfo.hostAddresses)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+            discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord2)
+            nsdManager.unregisterService(registrationRecord3)
+        }
+    }
+
+    @Test
+    fun testAdvertising_registerServiceAndPublicKey_keyAnnounced() {
+        val si = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceType = serviceType
+            it.serviceName = serviceName
+            it.port = TEST_PORT
+            it.publicKey = publicKey
+        }
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        val registrationRecord = NsdRegistrationRecord()
+        val discoveryRecord = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord, si)
+
+            val announcement = packetReader.pollForReply(
+                "$serviceName.$serviceType.local",
+                TYPE_KEY
+            )
+            assertNotNull(announcement)
+            val keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
+            assertEquals(1, keyRecords.size)
+            val actualRecord = keyRecords.get(0)
+            assertEquals(TYPE_KEY, actualRecord.nsType)
+            assertEquals("$serviceName.$serviceType.local", actualRecord.dName)
+            assertEquals(NAME_RECORDS_TTL_MILLIS, actualRecord.ttl)
+            assertArrayEquals(publicKey, actualRecord.rr)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, discoveryRecord)
+
+            val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            val resolvedInfo1 = resolveService(discoveredInfo1)
+
+            assertEquals(serviceName, discoveredInfo1.serviceName)
+            assertEquals(TEST_PORT, resolvedInfo1.port)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord)
+
+            discoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testAdvertising_registerCustomHostAndPublicKey_keyAnnounced() {
+        val si = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.hostname = customHostname
+            it.hostAddresses = listOf(
+                    parseNumericAddress("192.0.2.23"),
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("2001:db8::2"))
+            it.publicKey = publicKey
+        }
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+                testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        val registrationRecord = NsdRegistrationRecord()
+        tryTest {
+            registerService(registrationRecord, si)
+
+            val announcement = packetReader.pollForReply("$customHostname.local", TYPE_KEY)
+            assertNotNull(announcement)
+            val keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
+            assertEquals(1, keyRecords.size)
+            val actualRecord = keyRecords.get(0)
+            assertEquals(TYPE_KEY, actualRecord.nsType)
+            assertEquals("$customHostname.local", actualRecord.dName)
+            assertEquals(NAME_RECORDS_TTL_MILLIS, actualRecord.ttl)
+            assertArrayEquals(publicKey, actualRecord.rr)
+
+            // This test case focuses on key announcement so we don't check the details of the
+            // announcement of the custom host addresses.
+            val addressRecords = announcement.records[ANSECTION].filter {
+                it.nsType == DnsResolver.TYPE_AAAA ||
+                        it.nsType == DnsResolver.TYPE_A
+            }
+            assertEquals(3, addressRecords.size)
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testAdvertising_registerTwoServicesWithSameCustomHostAndPublicKey_keyAnnounced() {
+        val si1 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceType = serviceType
+            it.serviceName = serviceName
+            it.port = TEST_PORT
+            it.hostname = customHostname
+            it.hostAddresses = listOf(
+                parseNumericAddress("192.0.2.23"),
+                parseNumericAddress("2001:db8::1"),
+                parseNumericAddress("2001:db8::2"))
+            it.publicKey = publicKey
+        }
+        val si2 = NsdServiceInfo().also {
+            it.network = testNetwork1.network
+            it.serviceType = serviceType2
+            it.serviceName = serviceName2
+            it.port = TEST_PORT + 1
+            it.hostname = customHostname
+            it.hostAddresses = listOf()
+            it.publicKey = publicKey
+        }
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+            testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        val registrationRecord1 = NsdRegistrationRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+        tryTest {
+            registerService(registrationRecord1, si1)
+
+            var announcement =
+                packetReader.pollForReply("$serviceName.$serviceType.local", TYPE_KEY)
+            assertNotNull(announcement)
+            var keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
+            assertEquals(2, keyRecords.size)
+            assertTrue(keyRecords.any { it.dName == "$serviceName.$serviceType.local" })
+            assertTrue(keyRecords.any { it.dName == "$customHostname.local" })
+            assertTrue(keyRecords.all { it.ttl == NAME_RECORDS_TTL_MILLIS })
+            assertTrue(keyRecords.all { it.rr.contentEquals(publicKey) })
+
+            // This test case focuses on key announcement so we don't check the details of the
+            // announcement of the custom host addresses.
+            val addressRecords = announcement.records[ANSECTION].filter {
+                it.nsType == DnsResolver.TYPE_AAAA ||
+                        it.nsType == DnsResolver.TYPE_A
+            }
+            assertEquals(3, addressRecords.size)
+
+            registerService(registrationRecord2, si2)
+
+            announcement = packetReader.pollForReply("$serviceName2.$serviceType2.local", TYPE_KEY)
+            assertNotNull(announcement)
+            keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
+            assertEquals(2, keyRecords.size)
+            assertTrue(keyRecords.any { it.dName == "$serviceName2.$serviceType2.local" })
+            assertTrue(keyRecords.any { it.dName == "$customHostname.local" })
+            assertTrue(keyRecords.all { it.ttl == NAME_RECORDS_TTL_MILLIS })
+            assertTrue(keyRecords.all { it.rr.contentEquals(publicKey) })
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord1)
+            nsdManager.unregisterService(registrationRecord2)
+        }
+    }
+
+    @Test
+    fun testServiceTypeClientRemovedAfterSocketDestroyed() {
+        val si = makeTestServiceInfo(testNetwork1.network)
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        registerService(registrationRecord, si)
+        // Register multiple discovery requests.
+        val discoveryRecord1 = NsdDiscoveryRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        val discoveryRecord3 = NsdDiscoveryRecord()
+        nsdManager.discoverServices(
+            "_test1._tcp",
+            NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network,
+            { it.run() },
+            discoveryRecord1
+        )
+        nsdManager.discoverServices(
+            "_test2._tcp",
+            NsdManager.PROTOCOL_DNS_SD,
+                testNetwork1.network,
+            { it.run() },
+            discoveryRecord2
+        )
+        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord3)
+
+        tryTest {
+            discoveryRecord1.expectCallback<DiscoveryStarted>()
+            discoveryRecord2.expectCallback<DiscoveryStarted>()
+            discoveryRecord3.expectCallback<DiscoveryStarted>()
+            val foundInfo = discoveryRecord3.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            assertEquals(testNetwork1.network, foundInfo.network)
+            // Verify that associated ServiceTypeClients has been created for testNetwork1.
+            assertTrue("No serviceTypeClients for testNetwork1.",
+                    hasServiceTypeClientsForNetwork(
+                            getServiceTypeClients(), testNetwork1.network))
+
+            // Disconnect testNetwork1
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                testNetwork1.close(cm)
+            }
+
+            // Verify that no ServiceTypeClients for testNetwork1.
+            discoveryRecord3.expectCallback<ServiceLost>()
+            assertFalse("Still has serviceTypeClients for testNetwork1.",
+                    hasServiceTypeClientsForNetwork(
+                            getServiceTypeClients(), testNetwork1.network))
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord1)
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+            nsdManager.stopServiceDiscovery(discoveryRecord3)
+            discoveryRecord1.expectCallback<DiscoveryStopped>()
+            discoveryRecord2.expectCallback<DiscoveryStopped>()
+            discoveryRecord3.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
+    @Test
+    fun testAdvertiseServiceWithNoAttributes_TxtRecordIstNotEmpty() {
+        deviceConfigRule.setConfig(
+            NAMESPACE_TETHERING,
+            "test_nsd_avoid_advertising_empty_txt_records",
+            "1"
+        )
+        val packetReader = TapPacketReader(
+            Handler(handlerThread.looper),
+            testNetwork1.iface.fileDescriptor.fileDescriptor,
+            1500 /* maxPacketSize */
+        )
+        packetReader.startAsyncForTest()
+        handlerThread.waitForIdle(TIMEOUT_MS)
+
+        // Test behavior described in RFC6763 6.1: empty TXT records are not allowed, but TXT
+        // records with a zero length string are equivalent.
+        val si = makeTestServiceInfo(testNetwork1.network)
+        // Register service on testNetwork1
+        val registrationRecord = NsdRegistrationRecord()
+        registerService(registrationRecord, si)
+
+        tryTest {
+            val announcement =
+                packetReader.pollForReply("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT)
+            assertNotNull(announcement)
+            val txtRecords = announcement.records[ANSECTION].filter {
+                it.nsType == DnsResolver.TYPE_TXT
+            }
+            assertEquals(1, txtRecords.size)
+            // The TXT record should contain as single zero
+            assertContentEquals(byteArrayOf(0), txtRecords[0].rr)
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+            registrationRecord.expectCallback<ServiceUnregistered>()
+        }
+    }
+
+    private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
+        return clients.any { client -> client.substring(
+                client.indexOf("network=") + "network=".length,
+                client.indexOf("interfaceIndex=") - 1) == network.getNetId().toString()
+        }
+    }
+
+    /**
+     * Get ServiceTypeClient logs from the system dump servicediscovery section.
+     *
+     * The sample output:
+     *     ServiceTypeClient: Type{_nmt079019787._tcp.local} \
+     *         SocketKey{ network=116 interfaceIndex=68 } with 1 listeners.
+     *     ServiceTypeClient: Type{_nmt079019787._tcp.local} \
+     *         SocketKey{ network=115 interfaceIndex=67 } with 1 listeners.
+     */
+    private fun getServiceTypeClients(): List<String> {
+        return SystemUtil.runShellCommand(
+                InstrumentationRegistry.getInstrumentation(),
+            "dumpsys servicediscovery"
+        )
+                .split("\n").mapNotNull { line ->
+                    line.indexOf("ServiceTypeClient:").let { idx ->
+                        if (idx == -1) {
+                            null
+                        } else {
+                            line.substring(idx)
+                        }
+                    }
+                }
+    }
+
     private fun buildConflictingAnnouncement(): ByteBuffer {
         /*
         Generated with:
@@ -2114,9 +2653,11 @@
                     rclass=0x8001, port=31234, target='conflict.local', ttl=120)
         )).hex()
          */
-        val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000104e736454657" +
+        val mdnsPayload = HexDump.hexStringToByteArray(
+            "000084000000000100000000104e736454657" +
                 "3743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00002" +
-                "18001000000780016000000007a0208636f6e666c696374056c6f63616c00")
+                "18001000000780016000000007a0208636f6e666c696374056c6f63616c00"
+        )
         replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
 
         return buildMdnsPacket(mdnsPayload)
@@ -2130,9 +2671,11 @@
                     rdata='2001:db8::321')
         )).hex()
          */
-        val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000144e7364" +
+        val mdnsPayload = HexDump.hexStringToByteArray(
+            "000084000000000100000000144e7364" +
                 "54657374486f7374313233343536373839056c6f63616c00001c000100000078001020010db80000" +
-                "00000000000000000321")
+                "00000000000000000321"
+        )
         replaceCustomHostnameWithTestSuffix(mdnsPayload)
 
         return buildMdnsPacket(mdnsPayload)
@@ -2183,22 +2726,29 @@
         mdnsPayload: ByteArray,
         srcAddr: Inet6Address = testSrcAddr
     ): ByteBuffer {
-        val packetBuffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
-                IPPROTO_UDP, mdnsPayload.size)
+        val packetBuffer = PacketBuilder.allocate(
+            true /* hasEther */,
+            IPPROTO_IPV6,
+                IPPROTO_UDP,
+            mdnsPayload.size
+        )
         val packetBuilder = PacketBuilder(packetBuffer)
         // Multicast ethernet address for IPv6 to ff02::fb
         val multicastEthAddr = MacAddress.fromBytes(
-                byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte()))
+                byteArrayOf(0x33, 0x33, 0, 0, 0, 0xfb.toByte())
+        )
         packetBuilder.writeL2Header(
                 MacAddress.fromBytes(byteArrayOf(1, 2, 3, 4, 5, 6)) /* srcMac */,
                 multicastEthAddr,
-                ETH_P_IPV6.toShort())
+                ETH_P_IPV6.toShort()
+        )
         packetBuilder.writeIpv6Header(
                 0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
                 IPPROTO_UDP.toByte(),
                 64 /* hop limit */,
                 srcAddr,
-                multicastIpv6Addr /* dstIp */)
+                multicastIpv6Addr /* dstIp */
+        )
         packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
         packetBuffer.put(mdnsPayload)
         return packetBuilder.finalizePacket()
@@ -2270,4 +2820,4 @@
     // No duplicate addresses in the actual address list
     assertEquals(actual.toSet().size, actual.size)
     assertEquals(expected.toSet(), actual.toSet())
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 3d828a4..0dd2a23 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -93,7 +93,7 @@
     private static final int SOCKET_TIMEOUT_MS = 10_000;
     private static final int PRIVATE_DNS_PROBE_MS = 1_000;
 
-    private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 10_000;
+    private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 30_000;
     private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30;
 
     private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
@@ -408,40 +408,10 @@
         return network;
     }
 
-    public Network connectToCell() throws InterruptedException {
-        if (cellConnectAttempted()) {
-            mCm.unregisterNetworkCallback(mCellNetworkCallback);
-        }
-        NetworkRequest cellRequest = new NetworkRequest.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .addCapability(NET_CAPABILITY_INTERNET)
-                .build();
-        mCellNetworkCallback = new TestNetworkCallback();
-        mCm.requestNetwork(cellRequest, mCellNetworkCallback);
-        final Network cellNetwork = mCellNetworkCallback.waitForAvailable();
-        assertNotNull("Cell network not available. " +
-                "Please ensure the device has working mobile data.", cellNetwork);
-        return cellNetwork;
-    }
-
-    public void disconnectFromCell() {
-        if (!cellConnectAttempted()) {
-            throw new IllegalStateException("Cell connection not attempted");
-        }
-        mCm.unregisterNetworkCallback(mCellNetworkCallback);
-        mCellNetworkCallback = null;
-    }
-
     public boolean cellConnectAttempted() {
         return mCellNetworkCallback != null;
     }
 
-    public void tearDown() {
-        if (cellConnectAttempted()) {
-            disconnectFromCell();
-        }
-    }
-
     private NetworkRequest makeWifiNetworkRequest() {
         return new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
diff --git a/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
new file mode 100644
index 0000000..32d6899
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/EthernetTestInterface.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts.util
+
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.IpConfiguration
+import android.net.TestNetworkInterface
+import android.net.cts.util.EthernetTestInterface.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Handler
+import android.util.Log
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.TapPacketReader
+import com.android.testutils.runAsShell
+import com.android.testutils.waitForIdle
+import java.net.NetworkInterface
+import kotlin.concurrent.Volatile
+import kotlin.test.assertNotNull
+
+private const val TAG = "EthernetTestInterface"
+private const val TIMEOUT_MS = 5_000L
+
+/**
+ * A class to manage the lifecycle of an ethernet interface.
+ *
+ * This class encapsulates creating new tun/tap interfaces and registering them with ethernet
+ * service.
+ */
+class EthernetTestInterface(
+    private val context: Context,
+    private val handler: Handler,
+    val testIface: TestNetworkInterface
+) {
+    private class EthernetStateListener(private val trackedIface: String) : InterfaceStateListener {
+        val events = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+        sealed class CallbackEntry {
+            data class InterfaceStateChanged(
+                val iface: String,
+                val state: Int,
+                val role: Int,
+                val cfg: IpConfiguration?
+            ) : CallbackEntry()
+        }
+
+        override fun onInterfaceStateChanged(
+            iface: String,
+            state: Int,
+            role: Int,
+            cfg: IpConfiguration?
+        ) {
+            // filter out callbacks for other interfaces
+            if (iface != trackedIface) return
+            events.add(InterfaceStateChanged(iface, state, role, cfg))
+        }
+
+        fun eventuallyExpect(state: Int) {
+            val cb = events.poll(TIMEOUT_MS) { it is InterfaceStateChanged && it.state == state }
+            assertNotNull(cb, "Never received state $state. Got: ${events.backtrace()}")
+        }
+    }
+
+    val name get() = testIface.interfaceName
+    val mtu: Int
+        get() {
+            val nif = NetworkInterface.getByName(name)
+            assertNotNull(nif)
+            return nif.mtu
+        }
+    val packetReader = TapPacketReader(handler, testIface.fileDescriptor.fileDescriptor, mtu)
+    private val listener = EthernetStateListener(name)
+    private val em = context.getSystemService(EthernetManager::class.java)!!
+    @Volatile private var cleanedUp = false
+
+    init{
+        em.addInterfaceStateListener(handler::post, listener)
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(true)
+        }
+        // Wait for link up to be processed in EthernetManager before returning.
+        listener.eventuallyExpect(STATE_LINK_UP)
+        handler.post { packetReader.start() }
+        handler.waitForIdle(TIMEOUT_MS)
+    }
+
+    fun destroy() {
+        // packetReader.stop() closes the test interface.
+        handler.post { packetReader.stop() }
+        handler.waitForIdle(TIMEOUT_MS)
+        listener.eventuallyExpect(STATE_ABSENT)
+
+        // setIncludeTestInterfaces() posts on the handler and does not run synchronously. However,
+        // there should be no need for a synchronization mechanism here. If the next test is
+        // bringing up its interface, a RTM_NEWLINK will be put on the back of the handler and is
+        // guaranteed to be in order with (i.e. after) this call, so there is no race here.
+        runAsShell(NETWORK_SETTINGS) {
+            em.setIncludeTestInterfaces(false)
+        }
+        em.removeInterfaceStateListener(listener)
+
+        cleanedUp = true
+    }
+
+    protected fun finalize() {
+        if (!cleanedUp) {
+            Log.wtf(TAG, "destroy() was not called for interface $name.")
+            destroy()
+        }
+    }
+}
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index 7d5ca2f..e0424ac 100644
--- a/tests/cts/netpermission/internetpermission/Android.bp
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -31,4 +31,5 @@
         "general-tests",
     ],
     host_required: ["net-tests-utils-host-common"],
+    sdk_version: "test_current",
 }
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 2fde1ce..689ce74 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -19,11 +19,17 @@
 
 android_test {
     name: "CtsNetTestCasesUpdateStatsPermission",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
 
     srcs: ["src/**/*.java"],
-
-    static_libs: ["ctstestrunner-axt"],
+    platform_apis: true,
+    static_libs: [
+        "ctstestrunner-axt",
+        "net-tests-utils",
+    ],
 
     // Tag this module as a cts test artifact
     test_suites: [
diff --git a/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
index bea843c..56bad31 100644
--- a/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
+++ b/tests/cts/netpermission/updatestatspermission/src/android/net/cts/network/permission/UpdateStatsPermissionTest.java
@@ -16,6 +16,10 @@
 
 package android.net.cts.networkpermission.updatestatspermission;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -25,6 +29,8 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -67,6 +73,11 @@
         out.write(buf);
         out.close();
         socket.close();
+        // Clear TrafficStats cache is needed to avoid rate-limit caching for
+        // TrafficStats API results on V+ devices.
+        if (SdkLevel.isAtLeastV()) {
+            runAsShell(NETWORK_SETTINGS, () -> TrafficStats.clearRateLimitCaches());
+        }
         long uidTxBytesAfter = TrafficStats.getUidTxBytes(Process.myUid());
         long uidTxDeltaBytes = uidTxBytesAfter - uidTxBytesBefore;
         assertTrue("uidtxb: " + uidTxBytesBefore + " -> " + uidTxBytesAfter + " delta="
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 3928961..1023173 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -47,6 +47,7 @@
 
     // Change to system current when TetheringManager move to bootclass path.
     platform_apis: true,
+    min_sdk_version: "30",
     host_required: ["net-tests-utils-host-common"],
 }
 
@@ -80,8 +81,8 @@
 
 // Tethering CTS tests for development and release. These tests always target the platform SDK
 // version, and are subject to all the restrictions appropriate to that version. Before SDK
-// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
-// devices.
+// finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
+// release devices as their min_sdk_version is set to a production version.
 android_test {
     name: "CtsTetheringTest",
     defaults: ["CtsTetheringTestDefaults"],
@@ -93,6 +94,14 @@
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
+        "mts-dnsresolver",
+        "mts-networking",
+        "mts-tethering",
+        "mts-wifi",
+        "mcts-dnsresolver",
+        "mcts-networking",
+        "mcts-tethering",
+        "mcts-wifi",
         "general-tests",
     ],
 
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 81608f7..8794847 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -235,6 +235,12 @@
         assertEquals(TETHERING_USB, tr2.getTetheringType());
         assertTrue(tr2.isExemptFromEntitlementCheck());
         assertFalse(tr2.getShouldShowEntitlementUi());
+
+        final TetheringRequest tr3 = new TetheringRequest.Builder(TETHERING_USB)
+                .setStaticIpv4Addresses(localAddr, clientAddr)
+                .setExemptFromEntitlementCheck(true)
+                .setShouldShowEntitlementUi(false).build();
+        assertEquals(tr2, tr3);
     }
 
     @Test
@@ -246,15 +252,7 @@
                 .setExemptFromEntitlementCheck(true)
                 .setShouldShowEntitlementUi(false).build();
         final TetheringRequest parceled = ParcelUtils.parcelingRoundTrip(unparceled);
-        assertEquals(unparceled.getTetheringType(), parceled.getTetheringType());
-        assertEquals(unparceled.getConnectivityScope(), parceled.getConnectivityScope());
-        assertEquals(unparceled.getLocalIpv4Address(), parceled.getLocalIpv4Address());
-        assertEquals(unparceled.getClientStaticIpv4Address(),
-                parceled.getClientStaticIpv4Address());
-        assertEquals(unparceled.isExemptFromEntitlementCheck(),
-                parceled.isExemptFromEntitlementCheck());
-        assertEquals(unparceled.getShouldShowEntitlementUi(),
-                parceled.getShouldShowEntitlementUi());
+        assertEquals(unparceled, parceled);
     }
 
     @Test
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 361d68c..06bdca6 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -27,6 +27,7 @@
 import android.net.ConnectivityManager
 import android.net.IDnsResolver
 import android.net.INetd
+import android.net.INetd.PERMISSION_INTERNET
 import android.net.LinkProperties
 import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
@@ -66,6 +67,8 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.tryTest
+import java.util.function.BiConsumer
+import java.util.function.Consumer
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
@@ -87,8 +90,6 @@
 import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 import org.mockito.Spy
-import java.util.function.Consumer
-import java.util.function.BiConsumer
 
 const val SERVICE_BIND_TIMEOUT_MS = 5_000L
 const val TEST_TIMEOUT_MS = 10_000L
@@ -225,6 +226,10 @@
         override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
         override fun makeNetIdManager() = TestNetIdManager()
         override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+                .also {
+                    doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
+                }
+        override fun isChangeEnabled(changeId: Long, uid: Int) = true
 
         override fun makeMultinetworkPolicyTracker(
             c: Context,
@@ -242,18 +247,19 @@
             super.makeHandlerThread(tag).also { handlerThreads.add(it) }
 
         override fun makeCarrierPrivilegeAuthenticator(
-            context: Context,
-            tm: TelephonyManager,
-            requestRestrictedWifiEnabled: Boolean,
-            listener: BiConsumer<Int, Int>
+                context: Context,
+                tm: TelephonyManager,
+                requestRestrictedWifiEnabled: Boolean,
+                listener: BiConsumer<Int, Int>,
+                handler: Handler
         ): CarrierPrivilegeAuthenticator {
             return CarrierPrivilegeAuthenticator(context,
-                object : CarrierPrivilegeAuthenticator.Dependencies() {
-                    override fun makeHandlerThread(): HandlerThread =
-                        super.makeHandlerThread().also { handlerThreads.add(it) }
-                },
-                tm, TelephonyManagerShimImpl.newInstance(tm),
-                requestRestrictedWifiEnabled, listener)
+                    object : CarrierPrivilegeAuthenticator.Dependencies() {
+                        override fun makeHandlerThread(): HandlerThread =
+                                super.makeHandlerThread().also { handlerThreads.add(it) }
+                    },
+                    tm, TelephonyManagerShimImpl.newInstance(tm),
+                    requestRestrictedWifiEnabled, listener, handler)
         }
 
         override fun makeSatelliteAccessController(
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
index 104d063..3d948ba 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
@@ -18,10 +18,14 @@
 
 import android.app.Service
 import android.content.Intent
+import androidx.annotation.GuardedBy
+import com.android.testutils.quitExecutorServices
+import com.android.testutils.quitThreads
 import java.net.URL
 import java.util.Collections
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.ExecutorService
 import kotlin.collections.ArrayList
 import kotlin.test.fail
 
@@ -37,7 +41,12 @@
                 .run {
                     withDefault { key -> getOrPut(key) { ConcurrentLinkedQueue() } }
                 }
-        private val httpRequestUrls = Collections.synchronizedList(ArrayList<String>())
+        private val httpRequestUrls = Collections.synchronizedList(mutableListOf<String>())
+
+        @GuardedBy("networkMonitorThreads")
+        private val networkMonitorThreads = mutableListOf<Thread>()
+        @GuardedBy("networkMonitorExecutorServices")
+        private val networkMonitorExecutorServices = mutableListOf<ExecutorService>()
 
         /**
          * Called when an HTTP request is being processed by NetworkMonitor. Returns the response
@@ -52,10 +61,47 @@
         }
 
         /**
+         * Called when NetworkMonitor creates a new Thread.
+         */
+        fun onNetworkMonitorThreadCreated(thread: Thread) {
+            synchronized(networkMonitorThreads) {
+                networkMonitorThreads.add(thread)
+            }
+        }
+
+        /**
+         * Called when NetworkMonitor creates a new ExecutorService.
+         */
+        fun onNetworkMonitorExecutorServiceCreated(executorService: ExecutorService) {
+            synchronized(networkMonitorExecutorServices) {
+                networkMonitorExecutorServices.add(executorService)
+            }
+        }
+
+        /**
          * Clear all state of this connector. This is intended for use between two tests, so all
          * state should be reset as if the connector was just created.
          */
         override fun clearAllState() {
+            quitThreads(
+                maxRetryCount = 3,
+                interrupt = true) {
+                synchronized(networkMonitorThreads) {
+                    networkMonitorThreads.toList().also { networkMonitorThreads.clear() }
+                }
+            }
+            quitExecutorServices(
+                maxRetryCount = 3,
+                // NetworkMonitor is expected to have interrupted its executors when probing
+                // finishes, otherwise it's a thread pool leak that should be caught, so they should
+                // not need to be interrupted (the test only needs to wait for them to finish).
+                interrupt = false) {
+                synchronized(networkMonitorExecutorServices) {
+                    networkMonitorExecutorServices.toList().also {
+                        networkMonitorExecutorServices.clear()
+                    }
+                }
+            }
             httpResponses.clear()
             httpRequestUrls.clear()
         }
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
index 765e56e..4780c5d 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
@@ -38,6 +38,7 @@
 import android.os.Build
 import android.os.Process
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
 import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.DOWNLOAD
 import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.UPLOAD
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
@@ -214,6 +215,11 @@
         // In practice, for one way 10k download payload, the download usage is about
         // 11222~12880 bytes, with 14~17 packets. And the upload usage is about 1279~1626 bytes
         // with 14~17 packets, which is majorly contributed by TCP ACK packets.
+        // Clear TrafficStats cache is needed to avoid rate-limit caching for
+        // TrafficStats API results on V+ devices.
+        if (SdkLevel.isAtLeastV()) {
+            TrafficStats.clearRateLimitCaches()
+        }
         val snapshotAfterDownload = StatsSnapshot(context, internalInterfaceName)
         val (expectedDownloadLower, expectedDownloadUpper) = getExpectedStatsBounds(
             TEST_DOWNLOAD_SIZE,
@@ -236,6 +242,9 @@
         )
 
         // Verify upload data usage accounting.
+        if (SdkLevel.isAtLeastV()) {
+            TrafficStats.clearRateLimitCaches()
+        }
         val snapshotAfterUpload = StatsSnapshot(context, internalInterfaceName)
         val (expectedUploadLower, expectedUploadUpper) = getExpectedStatsBounds(
             TEST_UPLOAD_SIZE,
@@ -299,7 +308,8 @@
         val buf = ByteArray(DEFAULT_BUFFER_SIZE)
 
         httpServer.addResponse(
-            TestHttpServer.Request(path, NanoHTTPD.Method.POST), NanoHTTPD.Response.Status.OK,
+            TestHttpServer.Request(path, NanoHTTPD.Method.POST),
+            NanoHTTPD.Response.Status.OK,
             content = getRandomString(downloadSize)
         )
         var httpConnection: HttpURLConnection? = null
@@ -349,15 +359,19 @@
     ) {
         operator fun plus(other: BareStats): BareStats {
             return BareStats(
-                this.rxBytes + other.rxBytes, this.rxPackets + other.rxPackets,
-                this.txBytes + other.txBytes, this.txPackets + other.txPackets
+                this.rxBytes + other.rxBytes,
+                this.rxPackets + other.rxPackets,
+                this.txBytes + other.txBytes,
+                this.txPackets + other.txPackets
             )
         }
 
         operator fun minus(other: BareStats): BareStats {
             return BareStats(
-                this.rxBytes - other.rxBytes, this.rxPackets - other.rxPackets,
-                this.txBytes - other.txBytes, this.txPackets - other.txPackets
+                this.rxBytes - other.rxBytes,
+                this.rxPackets - other.rxPackets,
+                this.txBytes - other.txBytes,
+                this.txPackets - other.txPackets
             )
         }
 
@@ -405,8 +419,12 @@
         private fun getUidDetail(iface: String, tag: Int): BareStats {
             return getNetworkStatsThat(iface, tag) { nsm, template ->
                 nsm.queryDetailsForUidTagState(
-                    template, Long.MIN_VALUE, Long.MAX_VALUE,
-                    Process.myUid(), tag, Bucket.STATE_ALL
+                    template,
+                    Long.MIN_VALUE,
+                    Long.MAX_VALUE,
+                    Process.myUid(),
+                    tag,
+                    Bucket.STATE_ALL
                 )
             }
         }
@@ -498,28 +516,36 @@
         assertInRange(
             "Unexpected iface traffic stats",
             after.iface,
-            before.trafficStatsIface, after.trafficStatsIface,
-            lower, upper
+            before.trafficStatsIface,
+            after.trafficStatsIface,
+            lower,
+            upper
         )
         // Uid traffic stats are counted in both direction because the external network
         // traffic is also attributed to the test uid.
         assertInRange(
             "Unexpected uid traffic stats",
             after.iface,
-            before.trafficStatsUid, after.trafficStatsUid,
-            lower + lower.reverse(), upper + upper.reverse()
+            before.trafficStatsUid,
+            after.trafficStatsUid,
+            lower + lower.reverse(),
+            upper + upper.reverse()
         )
         assertInRange(
             "Unexpected non-tagged summary stats",
             after.iface,
-            before.statsSummary, after.statsSummary,
-            lower, upper
+            before.statsSummary,
+            after.statsSummary,
+            lower,
+            upper
         )
         assertInRange(
             "Unexpected non-tagged uid stats",
             after.iface,
-            before.statsUid, after.statsUid,
-            lower, upper
+            before.statsUid,
+            after.statsUid,
+            lower,
+            upper
         )
     }
 
@@ -546,14 +572,16 @@
         assertInRange(
             "Unexpected tagged summary stats",
             after.iface,
-            before.taggedSummary, after.taggedSummary,
+            before.taggedSummary,
+            after.taggedSummary,
             lower,
             upper
         )
         assertInRange(
             "Unexpected tagged uid stats: ${Process.myUid()}",
             after.iface,
-            before.taggedUid, after.taggedUid,
+            before.taggedUid,
+            after.taggedUid,
             lower,
             upper
         )
@@ -570,7 +598,8 @@
     ) {
         // Passing the value after operation and the value before operation to dump the actual
         // numbers if it fails.
-        assertTrue(checkInRange(before, after, lower, upper),
+        assertTrue(
+            checkInRange(before, after, lower, upper),
             "$tag on $iface: $after - $before is not within range [$lower, $upper]"
         )
     }
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ServiceManagerWrapperIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ServiceManagerWrapperIntegrationTest.kt
new file mode 100644
index 0000000..7e00ed2
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/ServiceManagerWrapperIntegrationTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.net.integrationtests
+
+import android.content.Context
+import android.os.Build
+import com.android.server.ServiceManagerWrapper
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Integration tests for {@link ServiceManagerWrapper}. */
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.S)
+@ConnectivityModuleTest
+class ServiceManagerWrapperIntegrationTest {
+    @Test
+    fun testWaitForService_successFullyRetrievesConnectivityServiceBinder() {
+        assertNotNull(ServiceManagerWrapper.waitForService(Context.CONNECTIVITY_SERVICE))
+    }
+}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
index 7e227c4..e43ce29 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
@@ -30,13 +30,14 @@
 import com.android.server.NetworkStackService.NetworkStackConnector
 import com.android.server.connectivity.NetworkMonitor
 import com.android.server.net.integrationtests.NetworkStackInstrumentationService.InstrumentationConnector
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
 import java.io.ByteArrayInputStream
 import java.net.HttpURLConnection
 import java.net.URL
 import java.nio.charset.StandardCharsets
+import java.util.concurrent.ExecutorService
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
 
 private const val TEST_NETID = 42
 
@@ -60,6 +61,10 @@
     private class NetworkMonitorDeps(private val privateDnsBypassNetwork: Network) :
             NetworkMonitor.Dependencies() {
         override fun getPrivateDnsBypassNetwork(network: Network?) = privateDnsBypassNetwork
+        override fun onThreadCreated(thread: Thread) =
+            InstrumentationConnector.onNetworkMonitorThreadCreated(thread)
+        override fun onExecutorServiceCreated(ecs: ExecutorService) =
+            InstrumentationConnector.onNetworkMonitorExecutorServiceCreated(ecs)
     }
 
     /**
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
index 336be2e..9d158fd 100644
--- a/tests/mts/Android.bp
+++ b/tests/mts/Android.bp
@@ -31,6 +31,8 @@
     header_libs: [
         "bpf_headers",
     ],
+    version_script: ":connectivity_mainline_test_map",
+    stl: "libc++_static",
     static_libs: [
         "libbase",
         "libmodules-utils-build",
@@ -38,6 +40,6 @@
     srcs: [
         "bpf_existence_test.cpp",
     ],
-    compile_multilib: "first",
+    compile_multilib: "both",
     min_sdk_version: "30", // Ensure test runs on R and above.
 }
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 51a4eca..29f5cd2 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -68,6 +68,8 @@
     TETHERING "map_offload_tether_upstream6_map",
     TETHERING "map_test_bitmap",
     TETHERING "map_test_tether_downstream6_map",
+    TETHERING "map_test_tether2_downstream6_map",
+    TETHERING "map_test_tether3_downstream6_map",
     TETHERING "prog_offload_schedcls_tether_downstream4_ether",
     TETHERING "prog_offload_schedcls_tether_downstream4_rawip",
     TETHERING "prog_offload_schedcls_tether_downstream6_ether",
@@ -141,6 +143,27 @@
     NETD "map_netd_packet_trace_ringbuf",
 };
 
+// Provided by *current* mainline module for V+ devices
+static const set<string> MAINLINE_FOR_V_PLUS = {
+    NETD "prog_netd_connect4_inet4_connect",
+    NETD "prog_netd_connect6_inet6_connect",
+    NETD "prog_netd_recvmsg4_udp4_recvmsg",
+    NETD "prog_netd_recvmsg6_udp6_recvmsg",
+    NETD "prog_netd_sendmsg4_udp4_sendmsg",
+    NETD "prog_netd_sendmsg6_udp6_sendmsg",
+};
+
+// Provided by *current* mainline module for V+ devices with 5.4+ kernels
+static const set<string> MAINLINE_FOR_V_5_4_PLUS = {
+    NETD "prog_netd_getsockopt_prog",
+    NETD "prog_netd_setsockopt_prog",
+};
+
+// Provided by *current* mainline module for U+ devices with 5.10+ kernels
+static const set<string> MAINLINE_FOR_V_5_10_PLUS = {
+    NETD "prog_netd_cgroupsockrelease_inet_release",
+};
+
 static void addAll(set<string>& a, const set<string>& b) {
     a.insert(b.begin(), b.end());
 }
@@ -188,6 +211,9 @@
 
     // V requires Linux Kernel 4.19+, but nothing (as yet) added or removed in V.
     if (IsAtLeastV()) ASSERT_TRUE(isAtLeastKernelVersion(4, 19, 0));
+    DO_EXPECT(IsAtLeastV(), MAINLINE_FOR_V_PLUS);
+    DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_V_5_4_PLUS);
+    DO_EXPECT(IsAtLeastV() && isAtLeastKernelVersion(5, 10, 0), MAINLINE_FOR_V_5_10_PLUS);
 
     for (const auto& file : mustExist) {
         EXPECT_EQ(0, access(file.c_str(), R_OK)) << file << " does not exist";
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index 2f66d17..39a08fa 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -17,20 +17,23 @@
         "connectivity_native_test.cpp",
     ],
     header_libs: ["bpf_connectivity_headers"],
+    version_script: ":connectivity_mainline_test_map",
+    stl: "libc++_static",
     shared_libs: [
-        "libbase",
         "libbinder_ndk",
+        "libcom.android.tethering.connectivity_native",
         "liblog",
         "libnetutils",
         "libprocessgroup",
     ],
     static_libs: [
         "connectivity_native_aidl_interface-lateststable-ndk",
+        "libbase",
         "libcutils",
         "libmodules-utils-build",
         "libutils",
     ],
-    compile_multilib: "first",
+    compile_multilib: "both",
 }
 
 filegroup {
diff --git a/tests/native/connectivity_native_test/AndroidTestTemplate.xml b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
index 44e35a9..4ea114c 100644
--- a/tests/native/connectivity_native_test/AndroidTestTemplate.xml
+++ b/tests/native/connectivity_native_test/AndroidTestTemplate.xml
@@ -15,8 +15,10 @@
 <configuration description="Configuration for connectivity {MODULE} tests">
     <option name="test-suite-tag" value="mts" />
     <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
-    <!-- The tested code is only part of a SDK 30+ module (Tethering) -->
-    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+    <!-- The tested library is only usable on U+ -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+    <!-- The tested library is only available with the primary abi -->
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
 
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
diff --git a/tests/native/connectivity_native_test/connectivity_native_test.cpp b/tests/native/connectivity_native_test/connectivity_native_test.cpp
index f62a30b..faaf150 100644
--- a/tests/native/connectivity_native_test/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test/connectivity_native_test.cpp
@@ -14,25 +14,17 @@
  * limitations under the License.
  */
 
+#include <android-modules-utils/sdk_level.h>
 #include <android/binder_manager.h>
 #include <android/binder_process.h>
-#include <android-modules-utils/sdk_level.h>
-#include <cutils/misc.h>  // FIRST_APPLICATION_UID
+#include <connectivity_native.h>
+#include <cutils/misc.h> // FIRST_APPLICATION_UID
 #include <dlfcn.h>
 #include <gtest/gtest.h>
 #include <netinet/in.h>
 
 #include "bpf/BpfUtils.h"
 
-typedef int (*GetPortsBlockedForBind)(in_port_t*, size_t*);
-GetPortsBlockedForBind getPortsBlockedForBind;
-typedef int (*BlockPortForBind)(in_port_t);
-BlockPortForBind blockPortForBind;
-typedef int (*UnblockPortForBind)(in_port_t);
-UnblockPortForBind unblockPortForBind;
-typedef int (*UnblockAllPortsForBind)();
-UnblockAllPortsForBind unblockAllPortsForBind;
-
 class ConnectivityNativeBinderTest : public ::testing::Test {
   public:
     in_port_t mActualBlockedPorts[65535];
@@ -50,29 +42,12 @@
         if (!android::bpf::isAtLeastKernelVersion(5, 4, 0))
             GTEST_SKIP() << "Kernel should be at least 5.4.";
 
-        // Necessary to use dlopen/dlsym since the lib is only available on U and there
-        // is no Sdk34ModuleController in tradefed yet.
-        // TODO: link against the library directly and add Sdk34ModuleController to
-        // AndroidTest.txml when available.
-        void* nativeLib = dlopen("libcom.android.tethering.connectivity_native.so", RTLD_NOW);
-        ASSERT_NE(nullptr, nativeLib);
-        getPortsBlockedForBind = reinterpret_cast<GetPortsBlockedForBind>(
-                dlsym(nativeLib, "AConnectivityNative_getPortsBlockedForBind"));
-        ASSERT_NE(nullptr, getPortsBlockedForBind);
-        blockPortForBind = reinterpret_cast<BlockPortForBind>(
-                dlsym(nativeLib, "AConnectivityNative_blockPortForBind"));
-        ASSERT_NE(nullptr, blockPortForBind);
-        unblockPortForBind = reinterpret_cast<UnblockPortForBind>(
-                dlsym(nativeLib, "AConnectivityNative_unblockPortForBind"));
-        ASSERT_NE(nullptr, unblockPortForBind);
-        unblockAllPortsForBind = reinterpret_cast<UnblockAllPortsForBind>(
-                dlsym(nativeLib, "AConnectivityNative_unblockAllPortsForBind"));
-        ASSERT_NE(nullptr, unblockAllPortsForBind);
-
-        // If there are already ports being blocked on device unblockAllPortsForBind() store
-        // the currently blocked ports and add them back at the end of the test. Do this for
-        // every test case so additional test cases do not forget to add ports back.
-        int err = getPortsBlockedForBind(mActualBlockedPorts, &mActualBlockedPortsCount);
+        // If there are already ports being blocked on device store the
+        // currently blocked ports and add them back at the end of the test. Do
+        // this for every test case so additional test cases do not forget to
+        // add ports back.
+        int err = AConnectivityNative_getPortsBlockedForBind(
+            mActualBlockedPorts, &mActualBlockedPortsCount);
         EXPECT_EQ(err, 0);
         restoreBlockedPorts = true;
     }
@@ -81,8 +56,9 @@
         int err;
         if (mActualBlockedPortsCount > 0 && restoreBlockedPorts) {
             for (int i=0; i < mActualBlockedPortsCount; i++) {
-                err = blockPortForBind(mActualBlockedPorts[i]);
-                EXPECT_EQ(err, 0);
+              err =
+                  AConnectivityNative_blockPortForBind(mActualBlockedPorts[i]);
+              EXPECT_EQ(err, 0);
             }
         }
     }
@@ -99,7 +75,7 @@
         int blockedPort = 0;
         if (blockPort) {
             blockedPort = ntohs(port);
-            err = blockPortForBind(blockedPort);
+            err = AConnectivityNative_blockPortForBind(blockedPort);
             EXPECT_EQ(err, 0);
         }
 
@@ -107,7 +83,7 @@
 
         if (blockPort) {
             EXPECT_EQ(-1, sock3);
-            err = unblockPortForBind(blockedPort);
+            err = AConnectivityNative_unblockPortForBind(blockedPort);
             EXPECT_EQ(err, 0);
         } else {
             EXPECT_NE(-1, sock3);
@@ -197,11 +173,11 @@
 }
 
 TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) {
-    int err = blockPortForBind(5555);
+    int err = AConnectivityNative_blockPortForBind(5555);
     EXPECT_EQ(err, 0);
-    err = blockPortForBind(5555);
+    err = AConnectivityNative_blockPortForBind(5555);
     EXPECT_EQ(err, 0);
-    err = unblockPortForBind(5555);
+    err = AConnectivityNative_unblockPortForBind(5555);
     EXPECT_EQ(err, 0);
 }
 
@@ -210,16 +186,17 @@
     in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
 
     if (mActualBlockedPortsCount > 0) {
-        err = unblockAllPortsForBind();
+        err = AConnectivityNative_unblockAllPortsForBind();
     }
 
     for (int i : blockedPorts) {
-        err = blockPortForBind(i);
+        err = AConnectivityNative_blockPortForBind(i);
         EXPECT_EQ(err, 0);
     }
     size_t actualBlockedPortsCount = 8;
     in_port_t actualBlockedPorts[actualBlockedPortsCount];
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_NE(actualBlockedPortsCount, 0);
     for (int i=0; i < actualBlockedPortsCount; i++) {
@@ -227,9 +204,10 @@
     }
 
     // Remove the ports we added.
-    err = unblockAllPortsForBind();
+    err = AConnectivityNative_unblockAllPortsForBind();
     EXPECT_EQ(err, 0);
-    err = getPortsBlockedForBind(actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(actualBlockedPorts,
+                                                     &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 0);
 }
@@ -239,23 +217,25 @@
     in_port_t blockedPorts[8] = {1, 100, 1220, 1333, 2700, 5555, 5600, 65000};
 
     if (mActualBlockedPortsCount > 0) {
-        err = unblockAllPortsForBind();
+        err = AConnectivityNative_unblockAllPortsForBind();
     }
 
     for (int i : blockedPorts) {
-        err = blockPortForBind(i);
+        err = AConnectivityNative_blockPortForBind(i);
         EXPECT_EQ(err, 0);
     }
 
     size_t actualBlockedPortsCount = 8;
     in_port_t actualBlockedPorts[actualBlockedPortsCount];
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 8);
 
-    err = unblockAllPortsForBind();
+    err = AConnectivityNative_unblockAllPortsForBind();
     EXPECT_EQ(err, 0);
-    err = getPortsBlockedForBind((in_port_t*) actualBlockedPorts, &actualBlockedPortsCount);
+    err = AConnectivityNative_getPortsBlockedForBind(
+        (in_port_t *)actualBlockedPorts, &actualBlockedPortsCount);
     EXPECT_EQ(err, 0);
     EXPECT_EQ(actualBlockedPortsCount, 0);
     // If mActualBlockedPorts is not empty, ports will be added back in teardown.
@@ -264,7 +244,7 @@
 TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
     int curUid = getuid();
     EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
-    int err = blockPortForBind((in_port_t) 5555);
+    int err = AConnectivityNative_blockPortForBind((in_port_t)5555);
     EXPECT_EQ(EPERM, err);
     EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
 }
diff --git a/tests/native/utilities/firewall.h b/tests/native/utilities/firewall.h
index b3d69bf..a5cb0b9 100644
--- a/tests/native/utilities/firewall.h
+++ b/tests/native/utilities/firewall.h
@@ -18,6 +18,7 @@
 #pragma once
 
 #include <android-base/thread_annotations.h>
+#define BPF_MAP_LOCKLESS_FOR_TEST
 #include <bpf/BpfMap.h>
 #include "netd.h"
 
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 20d457f..ef3ebb0 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -58,14 +58,11 @@
 filegroup {
     name: "non-connectivity-module-test",
     srcs: [
-        "java/android/net/Ikev2VpnProfileTest.java",
         "java/android/net/IpMemoryStoreTest.java",
         "java/android/net/TelephonyNetworkSpecifierTest.java",
-        "java/android/net/VpnManagerTest.java",
         "java/android/net/ipmemorystore/*.java",
         "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
         "java/com/android/internal/net/NetworkUtilsInternalTest.java",
-        "java/com/android/internal/net/VpnProfileTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
         "java/com/android/server/connectivity/MetricsTestUtil.java",
@@ -75,17 +72,6 @@
     ],
 }
 
-// Subset of services-core used to by ConnectivityService tests to test VPN realistically.
-// This is stripped by jarjar (see rules below) from other unrelated classes, so tests do not
-// include most classes from services-core, which are unrelated and cause wrong code coverage
-// calculations.
-java_library {
-    name: "services.core-vpn",
-    static_libs: ["services.core"],
-    jarjar_rules: "vpn-jarjar-rules.txt",
-    visibility: ["//visibility:private"],
-}
-
 java_defaults {
     name: "FrameworksNetTestsDefaults",
     min_sdk_version: "30",
@@ -106,13 +92,13 @@
         "frameworks-net-integration-testutils",
         "framework-protos",
         "mockito-target-minus-junit4",
+        "modules-utils-build",
         "net-tests-utils",
         "net-utils-services-common",
         "platform-compat-test-rules",
         "platform-test-annotations",
         "service-connectivity-pre-jarjar",
         "service-connectivity-tiramisu-pre-jarjar",
-        "services.core-vpn",
         "testables",
         "cts-net-utils",
     ],
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b71a46f..9a77c89 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -52,6 +52,7 @@
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -72,6 +73,7 @@
 import android.os.Messenger;
 import android.os.Process;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
@@ -83,6 +85,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -240,7 +243,7 @@
 
         // register callback
         when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
-                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(request);
+                anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(request);
         manager.requestNetwork(request, callback, handler);
 
         // callback triggers
@@ -269,7 +272,7 @@
 
         // register callback
         when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
-                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req1);
+                anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(req1);
         manager.requestNetwork(req1, callback, handler);
 
         // callback triggers
@@ -287,7 +290,7 @@
 
         // callback can be registered again
         when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
-                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req2);
+                anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(req2);
         manager.requestNetwork(req2, callback, handler);
 
         // callback triggers
@@ -311,7 +314,7 @@
 
         when(mCtx.getApplicationInfo()).thenReturn(info);
         when(mService.requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(),
-                anyInt(), any(), nullable(String.class))).thenReturn(request);
+                anyInt(), any(), nullable(String.class), anyInt())).thenReturn(request);
 
         Handler handler = new Handler(Looper.getMainLooper());
         manager.requestNetwork(request, callback, handler);
@@ -403,15 +406,15 @@
         manager.requestNetwork(request, callback);
         verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities),
                 eq(REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
 
         // Verify that register network callback does not calls requestNetwork at all.
         manager.registerNetworkCallback(request, callback);
         verify(mService, never()).requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(),
-                anyInt(), anyInt(), any(), any());
+                anyInt(), anyInt(), any(), any(), anyInt());
         verify(mService).listenForNetwork(eq(request.networkCapabilities), any(), any(), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
 
         Handler handler = new Handler(ConnectivityThread.getInstanceLooper());
@@ -419,24 +422,24 @@
         manager.registerDefaultNetworkCallback(callback);
         verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null),
                 eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
 
         manager.registerDefaultNetworkCallbackForUid(42, callback, handler);
         verify(mService).requestNetwork(eq(42), eq(null),
                 eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
 
         manager.requestBackgroundNetwork(request, callback, handler);
         verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities),
                 eq(BACKGROUND_REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
 
         manager.registerSystemDefaultNetworkCallback(callback, handler);
         verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null),
                 eq(TRACK_SYSTEM_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
     }
 
@@ -516,16 +519,154 @@
                     + " attempts", ref.get());
     }
 
-    private <T> void mockService(Class<T> clazz, String name, T service) {
-        doReturn(service).when(mCtx).getSystemService(name);
-        doReturn(name).when(mCtx).getSystemServiceName(clazz);
+    @Test
+    public void testDeclaredMethodsFlag_requestWithMixedMethods_RegistrationFlagsMatch()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
 
-        // If the test suite uses the inline mock maker library, such as for coverage tests,
-        // then the final version of getSystemService must also be mocked, as the real
-        // method will not be called by the test and null object is returned since no mock.
-        // Otherwise, mocking a final method will fail the test.
-        if (mCtx.getSystemService(clazz) == null) {
-            doReturn(service).when(mCtx).getSystemService(clazz);
-        }
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        final NetworkCallback callback1 = new ConnectivityManager.NetworkCallback() {
+            @Override
+            public void onPreCheck(@NonNull Network network) {}
+            @Override
+            public void onAvailable(@NonNull Network network) {}
+            @Override
+            public void onLost(@NonNull Network network) {}
+            @Override
+            public void onCapabilitiesChanged(@NonNull Network network,
+                    @NonNull NetworkCapabilities networkCapabilities) {}
+            @Override
+            public void onLocalNetworkInfoChanged(@NonNull Network network,
+                    @NonNull LocalNetworkInfo localNetworkInfo) {}
+            @Override
+            public void onNetworkResumed(@NonNull Network network) {}
+            @Override
+            public void onBlockedStatusChanged(@NonNull Network network, int blocked) {}
+        };
+        manager.requestNetwork(request, callback1);
+
+        final InOrder inOrder = inOrder(mService);
+        inOrder.verify(mService).requestNetwork(
+                anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+                eq(1 << ConnectivityManager.CALLBACK_PRECHECK
+                        | 1 << ConnectivityManager.CALLBACK_AVAILABLE
+                        | 1 << ConnectivityManager.CALLBACK_LOST
+                        | 1 << ConnectivityManager.CALLBACK_CAP_CHANGED
+                        | 1 << ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED
+                        | 1 << ConnectivityManager.CALLBACK_RESUMED
+                        | 1 << ConnectivityManager.CALLBACK_BLK_CHANGED));
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_listenWithMixedMethods_RegistrationFlagsMatch()
+            throws Exception {
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+
+        final NetworkCallback callback2 = new ConnectivityManager.NetworkCallback() {
+            @Override
+            public void onLosing(@NonNull Network network, int maxMsToLive) {}
+            @Override
+            public void onUnavailable() {}
+            @Override
+            public void onLinkPropertiesChanged(@NonNull Network network,
+                    @NonNull LinkProperties linkProperties) {}
+            @Override
+            public void onNetworkSuspended(@NonNull Network network) {}
+        };
+        manager.registerNetworkCallback(request, callback2);
+        // Call a second time with the same callback to exercise caching
+        manager.registerNetworkCallback(request, callback2);
+
+        verify(mService, times(2)).listenForNetwork(
+                any(), any(), any(), anyInt(), any(), any(),
+                eq(1 << ConnectivityManager.CALLBACK_LOSING
+                        // AVAILABLE calls IP_CHANGED and SUSPENDED so it gets added
+                        | 1 << ConnectivityManager.CALLBACK_AVAILABLE
+                        | 1 << ConnectivityManager.CALLBACK_UNAVAIL
+                        | 1 << ConnectivityManager.CALLBACK_IP_CHANGED
+                        | 1 << ConnectivityManager.CALLBACK_SUSPENDED));
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_requestWithHiddenAvailableCallback_RegistrationFlagsMatch()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+
+        final NetworkCallback hiddenOnAvailableCb = new ConnectivityManager.NetworkCallback() {
+            // This overload is @hide but might still be used by (bad) apps
+            @Override
+            public void onAvailable(@NonNull Network network,
+                    @NonNull NetworkCapabilities networkCapabilities,
+                    @NonNull LinkProperties linkProperties, boolean blocked) {}
+        };
+        manager.registerDefaultNetworkCallback(hiddenOnAvailableCb);
+
+        verify(mService).requestNetwork(
+                anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+                eq(1 << ConnectivityManager.CALLBACK_AVAILABLE));
+    }
+
+    public static class NetworkCallbackWithOnLostOnly extends NetworkCallback {
+        @Override
+        public void onLost(@NonNull Network network) {}
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_requestWithoutAvailableCallback_RegistrationFlagsMatch()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        final Handler handler = new Handler(Looper.getMainLooper());
+
+        final NetworkCallback noOnAvailableCb = new NetworkCallbackWithOnLostOnly();
+        manager.registerSystemDefaultNetworkCallback(noOnAvailableCb, handler);
+
+        verify(mService).requestNetwork(
+                anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+                eq(1 << ConnectivityManager.CALLBACK_LOST));
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_listenWithMock_OptimizationDisabled()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        final Handler handler = new Handler(Looper.getMainLooper());
+
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        manager.registerNetworkCallback(request, mock(NetworkCallbackWithOnLostOnly.class),
+                handler);
+
+        verify(mService).listenForNetwork(
+                any(), any(), any(), anyInt(), any(), any(),
+                // Mock that does not call the constructor -> do not use the optimization
+                eq(~0));
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_requestWitNoCallback_OptimizationDisabled()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        final Handler handler = new Handler(Looper.getMainLooper());
+
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        final NetworkCallback noCallbackAtAll = new ConnectivityManager.NetworkCallback() {};
+        manager.requestBackgroundNetwork(request, noCallbackAtAll, handler);
+
+        verify(mService).requestNetwork(
+                anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+                // No callbacks overridden -> do not use the optimization
+                eq(~0));
     }
 }
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
deleted file mode 100644
index e12e961..0000000
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
- * 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 android.net;
-
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6;
-import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
-
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.net.ipsec.ike.IkeKeyIdIdentification;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-import android.test.mock.MockContext;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.net.VpnProfile;
-import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
-import com.android.net.module.util.ProxyUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import javax.security.auth.x500.X500Principal;
-
-/** Unit tests for {@link Ikev2VpnProfile.Builder}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class Ikev2VpnProfileTest {
-    private static final String SERVER_ADDR_STRING = "1.2.3.4";
-    private static final String IDENTITY_STRING = "Identity";
-    private static final String USERNAME_STRING = "username";
-    private static final String PASSWORD_STRING = "pa55w0rd";
-    private static final String EXCL_LIST = "exclList";
-    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
-    private static final int TEST_MTU = 1300;
-
-    @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
-    private final MockContext mMockContext =
-            new MockContext() {
-                @Override
-                public String getOpPackageName() {
-                    return "fooPackage";
-                }
-            };
-    private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy(
-            SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST));
-
-    private X509Certificate mUserCert;
-    private X509Certificate mServerRootCa;
-    private PrivateKey mPrivateKey;
-
-    @Before
-    public void setUp() throws Exception {
-        mServerRootCa = generateRandomCertAndKeyPair().cert;
-
-        final CertificateAndKey userCertKey = generateRandomCertAndKeyPair();
-        mUserCert = userCertKey.cert;
-        mPrivateKey = userCertKey.key;
-    }
-
-    private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING);
-
-        builder.setBypassable(true);
-        builder.setProxy(mProxy);
-        builder.setMaxMtu(TEST_MTU);
-        builder.setMetered(true);
-
-        return builder;
-    }
-
-    @Test
-    public void testBuildValidProfileWithOptions() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-
-        // Check non-auth parameters correctly stored
-        assertEquals(SERVER_ADDR_STRING, profile.getServerAddr());
-        assertEquals(IDENTITY_STRING, profile.getUserIdentity());
-        assertEquals(mProxy, profile.getProxyInfo());
-        assertTrue(profile.isBypassable());
-        assertTrue(profile.isMetered());
-        assertEquals(TEST_MTU, profile.getMaxMtu());
-        assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms());
-    }
-
-    @Test
-    public void testBuildUsernamePasswordProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-
-        assertEquals(USERNAME_STRING, profile.getUsername());
-        assertEquals(PASSWORD_STRING, profile.getPassword());
-        assertEquals(mServerRootCa, profile.getServerRootCaCert());
-
-        assertNull(profile.getPresharedKey());
-        assertNull(profile.getRsaPrivateKey());
-        assertNull(profile.getUserCert());
-    }
-
-    @Test
-    public void testBuildDigitalSignatureProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-
-        assertEquals(profile.getUserCert(), mUserCert);
-        assertEquals(mPrivateKey, profile.getRsaPrivateKey());
-        assertEquals(profile.getServerRootCaCert(), mServerRootCa);
-
-        assertNull(profile.getPresharedKey());
-        assertNull(profile.getUsername());
-        assertNull(profile.getPassword());
-    }
-
-    @Test
-    public void testBuildPresharedKeyProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthPsk(PSK_BYTES);
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-
-        assertArrayEquals(PSK_BYTES, profile.getPresharedKey());
-
-        assertNull(profile.getServerRootCaCert());
-        assertNull(profile.getUsername());
-        assertNull(profile.getPassword());
-        assertNull(profile.getRsaPrivateKey());
-        assertNull(profile.getUserCert());
-    }
-
-    @Test
-    public void testBuildWithAllowedAlgorithmsAead() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAuthPsk(PSK_BYTES);
-
-        List<String> allowedAlgorithms =
-                Arrays.asList(
-                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
-                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305);
-        builder.setAllowedAlgorithms(allowedAlgorithms);
-
-        final Ikev2VpnProfile profile = builder.build();
-        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
-    }
-
-    @Test
-    public void testBuildWithAllowedAlgorithmsNormal() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAuthPsk(PSK_BYTES);
-
-        List<String> allowedAlgorithms =
-                Arrays.asList(
-                        IpSecAlgorithm.AUTH_HMAC_SHA512,
-                        IpSecAlgorithm.AUTH_AES_XCBC,
-                        IpSecAlgorithm.AUTH_AES_CMAC,
-                        IpSecAlgorithm.CRYPT_AES_CBC,
-                        IpSecAlgorithm.CRYPT_AES_CTR);
-        builder.setAllowedAlgorithms(allowedAlgorithms);
-
-        final Ikev2VpnProfile profile = builder.build();
-        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
-    }
-
-    @Test
-    public void testSetAllowedAlgorithmsEmptyList() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        try {
-            builder.setAllowedAlgorithms(new ArrayList<>());
-            fail("Expected exception due to no valid algorithm set");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testSetAllowedAlgorithmsInvalidList() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        List<String> allowedAlgorithms = new ArrayList<>();
-
-        try {
-            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256));
-            fail("Expected exception due to missing encryption");
-        } catch (IllegalArgumentException expected) {
-        }
-
-        try {
-            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC));
-            fail("Expected exception due to missing authentication");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        List<String> allowedAlgorithms = new ArrayList<>();
-
-        try {
-            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5));
-            fail("Expected exception due to insecure algorithm");
-        } catch (IllegalArgumentException expected) {
-        }
-
-        try {
-            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1));
-            fail("Expected exception due to insecure algorithm");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testBuildNoAuthMethodSet() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        try {
-            builder.build();
-            fail("Expected exception due to lack of auth method");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-
-    // TODO: Refer to Build.VERSION_CODES.SC_V2 when it's available in AOSP and mainline branch
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
-    @Test
-    public void testBuildExcludeLocalRoutesSet() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAuthPsk(PSK_BYTES);
-        builder.setLocalRoutesExcluded(true);
-
-        final Ikev2VpnProfile profile = builder.build();
-        assertNotNull(profile);
-        assertTrue(profile.areLocalRoutesExcluded());
-
-        builder.setBypassable(false);
-        try {
-            builder.build();
-            fail("Expected exception because excludeLocalRoutes should be set only"
-                    + " on the bypassable VPN");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testBuildInvalidMtu() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        try {
-            builder.setMaxMtu(500);
-            fail("Expected exception due to too-small MTU");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    private void verifyVpnProfileCommon(VpnProfile profile) {
-        assertEquals(SERVER_ADDR_STRING, profile.server);
-        assertEquals(IDENTITY_STRING, profile.ipsecIdentifier);
-        assertEquals(mProxy, profile.proxy);
-        assertTrue(profile.isBypassable);
-        assertTrue(profile.isMetered);
-        assertEquals(TEST_MTU, profile.maxMtu);
-    }
-
-    @Test
-    public void testPskConvertToVpnProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthPsk(PSK_BYTES);
-        final VpnProfile profile = builder.build().toVpnProfile();
-
-        verifyVpnProfileCommon(profile);
-        assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret);
-
-        // Check nothing else is set
-        assertEquals("", profile.username);
-        assertEquals("", profile.password);
-        assertEquals("", profile.ipsecUserCert);
-        assertEquals("", profile.ipsecCaCert);
-    }
-
-    @Test
-    public void testUsernamePasswordConvertToVpnProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final VpnProfile profile = builder.build().toVpnProfile();
-
-        verifyVpnProfileCommon(profile);
-        assertEquals(USERNAME_STRING, profile.username);
-        assertEquals(PASSWORD_STRING, profile.password);
-        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
-
-        // Check nothing else is set
-        assertEquals("", profile.ipsecUserCert);
-        assertEquals("", profile.ipsecSecret);
-    }
-
-    @Test
-    public void testRsaConvertToVpnProfile() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final VpnProfile profile = builder.build().toVpnProfile();
-
-        final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE
-                + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded());
-        verifyVpnProfileCommon(profile);
-        assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
-        assertEquals(
-                expectedSecret,
-                profile.ipsecSecret);
-        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
-
-        // Check nothing else is set
-        assertEquals("", profile.username);
-        assertEquals("", profile.password);
-    }
-
-    @Test
-    public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthPsk(PSK_BYTES);
-        final VpnProfile profile = builder.build().toVpnProfile();
-        profile.username = USERNAME_STRING;
-        profile.password = PASSWORD_STRING;
-        profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa);
-        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
-
-        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
-        assertNull(result.getUsername());
-        assertNull(result.getPassword());
-        assertNull(result.getUserCert());
-        assertNull(result.getRsaPrivateKey());
-        assertNull(result.getServerRootCaCert());
-    }
-
-    @Test
-    public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final VpnProfile profile = builder.build().toVpnProfile();
-        profile.ipsecSecret = new String(PSK_BYTES);
-        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
-
-        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
-        assertNull(result.getPresharedKey());
-        assertNull(result.getUserCert());
-        assertNull(result.getRsaPrivateKey());
-    }
-
-    @Test
-    public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final VpnProfile profile = builder.build().toVpnProfile();
-        profile.username = USERNAME_STRING;
-        profile.password = PASSWORD_STRING;
-
-        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
-        assertNull(result.getUsername());
-        assertNull(result.getPassword());
-        assertNull(result.getPresharedKey());
-    }
-
-    @Test
-    public void testPskConversionIsLossless() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthPsk(PSK_BYTES);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testUsernamePasswordConversionIsLossless() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testRsaConversionIsLossless() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testBuildWithIkeTunConnParamsConvertToVpnProfile() throws Exception {
-        // Special keyId that contains delimiter character of VpnProfile
-        final byte[] keyId = "foo\0bar".getBytes();
-        final IkeTunnelConnectionParams tunnelParams = new IkeTunnelConnectionParams(
-                getTestIkeSessionParams(true /* testIpv6 */, new IkeKeyIdIdentification(keyId)),
-                CHILD_PARAMS);
-        final Ikev2VpnProfile ikev2VpnProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
-        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
-
-        assertEquals(VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS, vpnProfile.type);
-
-        // Username, password, server, ipsecIdentifier, ipsecCaCert, ipsecSecret, ipsecUserCert and
-        // getAllowedAlgorithms should not be set if IkeTunnelConnectionParams is set.
-        assertEquals("", vpnProfile.server);
-        assertEquals("", vpnProfile.ipsecIdentifier);
-        assertEquals("", vpnProfile.username);
-        assertEquals("", vpnProfile.password);
-        assertEquals("", vpnProfile.ipsecCaCert);
-        assertEquals("", vpnProfile.ipsecSecret);
-        assertEquals("", vpnProfile.ipsecUserCert);
-        assertEquals(0, vpnProfile.getAllowedAlgorithms().size());
-
-        // IkeTunnelConnectionParams should stay the same.
-        assertEquals(tunnelParams, vpnProfile.ikeTunConnParams);
-
-        // Convert to disk-stable format and then back to Ikev2VpnProfile should be the same.
-        final VpnProfile decodedVpnProfile =
-                VpnProfile.decode(vpnProfile.key, vpnProfile.encode());
-        final Ikev2VpnProfile convertedIkev2VpnProfile =
-                Ikev2VpnProfile.fromVpnProfile(decodedVpnProfile);
-        assertEquals(ikev2VpnProfile, convertedIkev2VpnProfile);
-    }
-
-    @Test
-    public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
-        final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
-        // Config authentication related fields is not required while building with
-        // IkeTunnelConnectionParams.
-        final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testAutomaticNattAndIpVersionConversionIsLossless() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAutomaticNattKeepaliveTimerEnabled(true);
-        builder.setAutomaticIpVersionSelectionEnabled(true);
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
-    }
-
-    @Test
-    public void testAutomaticNattAndIpVersionDefaults() throws Exception {
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        final Ikev2VpnProfile ikeProfile = builder.build();
-
-        assertEquals(false, ikeProfile.isAutomaticNattKeepaliveTimerEnabled());
-        assertEquals(false, ikeProfile.isAutomaticIpVersionSelectionEnabled());
-    }
-
-    @Test
-    public void testEquals() throws Exception {
-        // Verify building without IkeTunnelConnectionParams
-        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
-        assertEquals(builder.build(), builder.build());
-
-        // Verify building with IkeTunnelConnectionParams
-        final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
-        final IkeTunnelConnectionParams tunnelParams2 =
-                new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
-        assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
-                new Ikev2VpnProfile.Builder(tunnelParams2).build());
-    }
-
-    @Test
-    public void testBuildProfileWithNullProxy() throws Exception {
-        final Ikev2VpnProfile ikev2VpnProfile =
-                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
-                        .setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa)
-                        .build();
-
-        // ProxyInfo should be null for the profile without setting ProxyInfo.
-        assertNull(ikev2VpnProfile.getProxyInfo());
-
-        // ProxyInfo should stay null after performing toVpnProfile() and fromVpnProfile()
-        final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
-        assertNull(vpnProfile.proxy);
-
-        final Ikev2VpnProfile convertedIkev2VpnProfile = Ikev2VpnProfile.fromVpnProfile(vpnProfile);
-        assertNull(convertedIkev2VpnProfile.getProxyInfo());
-    }
-
-    private static class CertificateAndKey {
-        public final X509Certificate cert;
-        public final PrivateKey key;
-
-        CertificateAndKey(X509Certificate cert, PrivateKey key) {
-            this.cert = cert;
-            this.key = key;
-        }
-    }
-
-    private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
-        final Date validityBeginDate =
-                new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
-        final Date validityEndDate =
-                new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));
-
-        // Generate a keypair
-        final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
-        keyPairGenerator.initialize(512);
-        final KeyPair keyPair = keyPairGenerator.generateKeyPair();
-
-        final X500Principal dnName = new X500Principal("CN=test.android.com");
-        final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
-        certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
-        certGen.setSubjectDN(dnName);
-        certGen.setIssuerDN(dnName);
-        certGen.setNotBefore(validityBeginDate);
-        certGen.setNotAfter(validityEndDate);
-        certGen.setPublicKey(keyPair.getPublic());
-        certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
-
-        final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
-        return new CertificateAndKey(cert, keyPair.getPrivate());
-    }
-}
diff --git a/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt b/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt
new file mode 100644
index 0000000..af06a64
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.ConnectivityManager.NetworkCallbackMethodsHolder
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doCallRealMethod
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.mockingDetails
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkCallbackFlagsTest {
+
+    // To avoid developers forgetting to update NETWORK_CB_METHODS when modifying NetworkCallbacks,
+    // or using wrong values, calculate it from annotations here and verify that it matches.
+    // This avoids the runtime cost of reflection, but still ensures that the list is correct.
+    @Test
+    fun testNetworkCallbackMethods_calculateFromAnnotations_matchesHardcodedList() {
+        val calculatedMethods = getNetworkCallbackMethodsFromAnnotations()
+        assertEquals(
+            calculatedMethods.toSet(),
+            NetworkCallbackMethodsHolder.NETWORK_CB_METHODS.map {
+                NetworkCallbackMethodWithEquals(
+                    it.mName,
+                    it.mParameterTypes.toList(),
+                    callbacksCallingThisMethod = it.mCallbacksCallingThisMethod
+                )
+            }.toSet()
+        )
+    }
+
+    data class NetworkCallbackMethodWithEquals(
+        val name: String,
+        val parameterTypes: List<Class<*>>,
+        val callbacksCallingThisMethod: Int
+    )
+
+    data class NetworkCallbackMethodBuilder(
+        val name: String,
+        val parameterTypes: List<Class<*>>,
+        val isFinal: Boolean,
+        val methodId: Int,
+        val mayCall: Set<Int>?,
+        var callbacksCallingThisMethod: Int
+    ) {
+        fun build() = NetworkCallbackMethodWithEquals(
+            name,
+            parameterTypes,
+            callbacksCallingThisMethod
+        )
+    }
+
+    /**
+     * Build [NetworkCallbackMethodsHolder.NETWORK_CB_METHODS] from [NetworkCallback] annotations.
+     */
+    private fun getNetworkCallbackMethodsFromAnnotations(): List<NetworkCallbackMethodWithEquals> {
+        val parsedMethods = mutableListOf<NetworkCallbackMethodBuilder>()
+        val methods = NetworkCallback::class.java.declaredMethods
+        methods.forEach { method ->
+            val cb = method.getAnnotation(
+                NetworkCallback.FilteredCallback::class.java
+            ) ?: return@forEach
+            val callbacksCallingThisMethod = if (cb.calledByCallbackId == 0) {
+                0
+            } else {
+                1 shl cb.calledByCallbackId
+            }
+            parsedMethods.add(
+                NetworkCallbackMethodBuilder(
+                    method.name,
+                    method.parameterTypes.toList(),
+                    Modifier.isFinal(method.modifiers),
+                    cb.methodId,
+                    cb.mayCall.toSet(),
+                    callbacksCallingThisMethod
+                )
+            )
+        }
+
+        // Propagate callbacksCallingThisMethod for transitive calls
+        do {
+            var hadChange = false
+            parsedMethods.forEach { caller ->
+                parsedMethods.forEach { callee ->
+                    if (caller.mayCall?.contains(callee.methodId) == true) {
+                        // Callbacks that call the caller also cause calls to the callee. So
+                        // callbacksCallingThisMethod for the callee should include
+                        // callbacksCallingThisMethod from the caller.
+                        val newValue =
+                            caller.callbacksCallingThisMethod or callee.callbacksCallingThisMethod
+                        hadChange = hadChange || callee.callbacksCallingThisMethod != newValue
+                        callee.callbacksCallingThisMethod = newValue
+                    }
+                }
+            }
+        } while (hadChange)
+
+        // Final methods may affect the flags for transitive calls, but cannot be overridden, so do
+        // not need to be in the list (no overridden method in NetworkCallback will match them).
+        return parsedMethods.filter { !it.isFinal }.map { it.build() }
+    }
+
+    @Test
+    fun testMethodsAreAnnotated() {
+        val annotations = NetworkCallback::class.java.declaredMethods.mapNotNull { method ->
+            if (!Modifier.isPublic(method.modifiers) && !Modifier.isProtected(method.modifiers)) {
+                return@mapNotNull null
+            }
+            val annotation = method.getAnnotation(NetworkCallback.FilteredCallback::class.java)
+            assertNotNull(annotation, "$method is missing the @FilteredCallback annotation")
+            return@mapNotNull annotation
+        }
+
+        annotations.groupingBy { it.methodId }.eachCount().forEach { (methodId, cnt) ->
+            assertEquals(1, cnt, "Method ID $methodId is used more than once in @FilteredCallback")
+        }
+    }
+
+    @Test
+    fun testObviousCalleesAreInAnnotation() {
+        NetworkCallback::class.java.declaredMethods.forEach { method ->
+            val annotation = method.getAnnotation(NetworkCallback.FilteredCallback::class.java)
+                ?: return@forEach
+            val missingFlags = getObviousCallees(method).toMutableSet().apply {
+                removeAll(annotation.mayCall.toSet())
+            }
+            val msg = "@FilteredCallback on $method is missing flags " +
+                    "$missingFlags in mayCall. There may be other " +
+                    "calls that are not detected if they are done conditionally."
+            assertEquals(emptySet(), missingFlags, msg)
+        }
+    }
+
+    /**
+     * Invoke the specified NetworkCallback method with mock arguments, return a set of transitively
+     * called methods.
+     *
+     * This provides an idea of which methods are transitively called by the specified method. It's
+     * not perfect as some callees could be called or not depending on the exact values of the mock
+     * arguments that are passed in (for example, onAvailable calls onNetworkSuspended only if the
+     * capabilities lack the NOT_SUSPENDED capability), but it should catch obvious forgotten calls.
+     */
+    private fun getObviousCallees(method: Method): Set<Int> {
+        // Create a mock NetworkCallback that mocks all methods except the one specified by the
+        // caller.
+        val mockCallback = mock(NetworkCallback::class.java)
+
+        if (!Modifier.isFinal(method.modifiers) ||
+            // The mock class will be NetworkCallback (not a subclass) if using mockito-inline,
+            // which mocks final methods too
+            mockCallback.javaClass == NetworkCallback::class.java) {
+            doCallRealMethod().`when`(mockCallback).let { mockObj ->
+                val anyArgs = method.parameterTypes.map { any(it) }
+                method.invoke(mockObj, *anyArgs.toTypedArray())
+            }
+        }
+
+        // Invoke the target method with mock parameters
+        val mockParameters = method.parameterTypes.map { getMockFor(method, it) }
+        method.invoke(mockCallback, *mockParameters.toTypedArray())
+
+        // Aggregate callees
+        val mockingDetails = mockingDetails(mockCallback)
+        return mockingDetails.invocations.mapNotNull { inv ->
+            if (inv.method == method) {
+                null
+            } else {
+                inv.method.getAnnotation(NetworkCallback.FilteredCallback::class.java)?.methodId
+            }
+        }.toSet()
+    }
+
+    private fun getMockFor(method: Method, c: Class<*>): Any {
+        if (!c.isPrimitive && !Modifier.isFinal(c.modifiers)) {
+            return mock(c)
+        }
+        return when (c) {
+            NetworkCapabilities::class.java -> NetworkCapabilities()
+            LinkProperties::class.java -> LinkProperties()
+            LocalNetworkInfo::class.java -> LocalNetworkInfo(null)
+            Boolean::class.java -> false
+            Int::class.java -> 0
+            else -> fail("No mock set for parameter type $c used in $method")
+        }
+    }
+}
diff --git a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
similarity index 72%
rename from tests/unit/java/android/net/BpfNetMapsReaderTest.kt
rename to tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
index 8919666..b5d78f3 100644
--- a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
+++ b/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
@@ -21,11 +21,13 @@
 import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY
 import android.net.BpfNetMapsConstants.DOZABLE_MATCH
 import android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH
-import android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH
+import android.net.BpfNetMapsConstants.PENALTY_BOX_ADMIN_MATCH
+import android.net.BpfNetMapsConstants.PENALTY_BOX_USER_MATCH
 import android.net.BpfNetMapsConstants.STANDBY_MATCH
 import android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY
 import android.net.BpfNetMapsUtils.getMatchByFirewallChain
 import android.os.Build.VERSION_CODES
+import android.os.Process.FIRST_APPLICATION_UID
 import com.android.net.module.util.IBpfMap
 import com.android.net.module.util.Struct.S32
 import com.android.net.module.util.Struct.U32
@@ -42,15 +44,15 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-private const val TEST_UID1 = 1234
+private const val TEST_UID1 = 11234
 private const val TEST_UID2 = TEST_UID1 + 1
 private const val TEST_UID3 = TEST_UID2 + 1
 private const val NO_IIF = 0
 
-// pre-T devices does not support Bpf.
+// NetworkStack can not use this before U due to b/326143935
 @RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(VERSION_CODES.S_V2)
-class BpfNetMapsReaderTest {
+@IgnoreUpTo(VERSION_CODES.TIRAMISU)
+class NetworkStackBpfNetMapsTest {
     @Rule
     @JvmField
     val ignoreRule = DevSdkIgnoreRule()
@@ -58,14 +60,15 @@
     private val testConfigurationMap: IBpfMap<S32, U32> = TestBpfMap()
     private val testUidOwnerMap: IBpfMap<S32, UidOwnerValue> = TestBpfMap()
     private val testDataSaverEnabledMap: IBpfMap<S32, U8> = TestBpfMap()
-    private val bpfNetMapsReader = BpfNetMapsReader(
-        TestDependencies(testConfigurationMap, testUidOwnerMap, testDataSaverEnabledMap))
+    private val bpfNetMapsReader = NetworkStackBpfNetMaps(
+        TestDependencies(testConfigurationMap, testUidOwnerMap, testDataSaverEnabledMap)
+    )
 
     class TestDependencies(
         private val configMap: IBpfMap<S32, U32>,
         private val uidOwnerMap: IBpfMap<S32, UidOwnerValue>,
         private val dataSaverEnabledMap: IBpfMap<S32, U8>
-    ) : BpfNetMapsReader.Dependencies() {
+    ) : NetworkStackBpfNetMaps.Dependencies() {
         override fun getConfigurationMap() = configMap
         override fun getUidOwnerMap() = uidOwnerMap
         override fun getDataSaverEnabledMap() = dataSaverEnabledMap
@@ -99,11 +102,20 @@
             Modifier.isStatic(it.modifiers) && it.name.startsWith("FIREWALL_CHAIN_")
         }
         // Verify the size matches, this also verifies no common item in allow and deny chains.
-        assertEquals(BpfNetMapsConstants.ALLOW_CHAINS.size +
-                BpfNetMapsConstants.DENY_CHAINS.size, declaredChains.size)
+        assertEquals(
+                BpfNetMapsConstants.ALLOW_CHAINS.size +
+                        BpfNetMapsConstants.DENY_CHAINS.size +
+                        BpfNetMapsConstants.METERED_ALLOW_CHAINS.size +
+                        BpfNetMapsConstants.METERED_DENY_CHAINS.size,
+            declaredChains.size
+        )
         declaredChains.forEach {
-            assertTrue(BpfNetMapsConstants.ALLOW_CHAINS.contains(it.get(null)) ||
-                    BpfNetMapsConstants.DENY_CHAINS.contains(it.get(null)))
+            assertTrue(
+                    BpfNetMapsConstants.ALLOW_CHAINS.contains(it.get(null)) ||
+                            BpfNetMapsConstants.METERED_ALLOW_CHAINS.contains(it.get(null)) ||
+                            BpfNetMapsConstants.DENY_CHAINS.contains(it.get(null)) ||
+                            BpfNetMapsConstants.METERED_DENY_CHAINS.contains(it.get(null))
+            )
         }
     }
 
@@ -117,11 +129,17 @@
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(newConfig))
     }
 
-    fun isUidNetworkingBlocked(uid: Int, metered: Boolean = false, dataSaver: Boolean = false) =
-            bpfNetMapsReader.isUidNetworkingBlocked(uid, metered, dataSaver)
+    private fun mockDataSaverEnabled(enabled: Boolean) {
+        val dataSaverValue = if (enabled) {DATA_SAVER_ENABLED} else {DATA_SAVER_DISABLED}
+        testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(dataSaverValue))
+    }
+
+    fun isUidNetworkingBlocked(uid: Int, metered: Boolean = false) =
+            bpfNetMapsReader.isUidNetworkingBlocked(uid, metered)
 
     @Test
     fun testIsUidNetworkingBlockedByFirewallChains_allowChain() {
+        mockDataSaverEnabled(enabled = false)
         // With everything disabled by default, verify the return value is false.
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
         assertFalse(isUidNetworkingBlocked(TEST_UID1))
@@ -141,6 +159,7 @@
 
     @Test
     fun testIsUidNetworkingBlockedByFirewallChains_denyChain() {
+        mockDataSaverEnabled(enabled = false)
         // Enable standby chain but does not provide denied list. Verify the network is allowed
         // for all uids.
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
@@ -162,47 +181,67 @@
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
         mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_POWERSAVE, true)
         mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY, true)
+        mockDataSaverEnabled(enabled = false)
         assertTrue(isUidNetworkingBlocked(TEST_UID1))
     }
 
     @IgnoreUpTo(VERSION_CODES.S_V2)
     @Test
     fun testIsUidNetworkingBlockedByDataSaver() {
+        mockDataSaverEnabled(enabled = false)
         // With everything disabled by default, verify the return value is false.
         testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
         assertFalse(isUidNetworkingBlocked(TEST_UID1, metered = true))
 
         // Add uid1 to penalty box, verify the network is blocked for uid1, while uid2 is not
         // affected.
-        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH))
+        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, PENALTY_BOX_USER_MATCH))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, PENALTY_BOX_ADMIN_MATCH))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        testUidOwnerMap.updateEntry(
+                S32(TEST_UID1),
+                UidOwnerValue(NO_IIF, PENALTY_BOX_USER_MATCH or PENALTY_BOX_ADMIN_MATCH)
+        )
         assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
         assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
 
         // Enable data saver, verify the network is blocked for uid1, uid2, but uid3 in happy box
         // is not affected.
+        mockDataSaverEnabled(enabled = true)
         testUidOwnerMap.updateEntry(S32(TEST_UID3), UidOwnerValue(NO_IIF, HAPPY_BOX_MATCH))
-        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
-        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
-        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
 
         // Add uid1 to happy box as well, verify nothing is changed because penalty box has higher
         // priority.
         testUidOwnerMap.updateEntry(
             S32(TEST_UID1),
-            UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH or HAPPY_BOX_MATCH)
+            UidOwnerValue(NO_IIF, PENALTY_BOX_USER_MATCH or HAPPY_BOX_MATCH)
         )
-        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
-        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
-        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
+        testUidOwnerMap.updateEntry(
+                S32(TEST_UID1),
+                UidOwnerValue(NO_IIF, PENALTY_BOX_ADMIN_MATCH or HAPPY_BOX_MATCH)
+        )
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
 
         // Enable doze mode, verify uid3 is blocked even if it is in happy box.
         mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
-        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
-        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
-        assertTrue(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID3, metered = true))
 
         // Disable doze mode and data saver, only uid1 which is in penalty box is blocked.
         mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, false)
+        mockDataSaverEnabled(enabled = false)
         assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
         assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
         assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
@@ -214,6 +253,24 @@
     }
 
     @Test
+    fun testIsUidNetworkingBlocked_SystemUid() {
+        mockDataSaverEnabled(enabled = false)
+        testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+        mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
+
+        for (uid in FIRST_APPLICATION_UID - 5..FIRST_APPLICATION_UID + 5) {
+            // system uid is not blocked regardless of firewall chains
+            val expectBlocked = uid >= FIRST_APPLICATION_UID
+            testUidOwnerMap.updateEntry(S32(uid), UidOwnerValue(NO_IIF, PENALTY_BOX_USER_MATCH))
+            assertEquals(
+                expectBlocked,
+                    isUidNetworkingBlocked(uid, metered = true),
+                    "isUidNetworkingBlocked returns unexpected value for uid = " + uid
+            )
+        }
+    }
+
+    @Test
     fun testGetDataSaverEnabled() {
         testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_DISABLED))
         assertFalse(bpfNetMapsReader.dataSaverEnabled)
diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java
deleted file mode 100644
index 2ab4e45..0000000
--- a/tests/unit/java/android/net/VpnManagerTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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 android.net;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assume.assumeFalse;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.test.mock.MockContext;
-import android.util.SparseArray;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.InstrumentationRegistry;
-
-import com.android.internal.net.VpnProfile;
-import com.android.internal.util.MessageUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Unit tests for {@link VpnManager}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class VpnManagerTest {
-
-    private static final String PKG_NAME = "fooPackage";
-
-    private static final String SESSION_NAME_STRING = "testSession";
-    private static final String SERVER_ADDR_STRING = "1.2.3.4";
-    private static final String IDENTITY_STRING = "Identity";
-    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
-
-    private IVpnManager mMockService;
-    private VpnManager mVpnManager;
-    private final MockContext mMockContext =
-            new MockContext() {
-                @Override
-                public String getOpPackageName() {
-                    return PKG_NAME;
-                }
-            };
-
-    @Before
-    public void setUp() throws Exception {
-        assumeFalse("Skipping test because watches don't support VPN",
-            InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature(
-                PackageManager.FEATURE_WATCH));
-        mMockService = mock(IVpnManager.class);
-        mVpnManager = new VpnManager(mMockContext, mMockService);
-    }
-
-    @Test
-    public void testProvisionVpnProfilePreconsented() throws Exception {
-        final PlatformVpnProfile profile = getPlatformVpnProfile();
-        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
-                .thenReturn(true);
-
-        // Expect there to be no intent returned, as consent has already been granted.
-        assertNull(mVpnManager.provisionVpnProfile(profile));
-        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
-    }
-
-    @Test
-    public void testProvisionVpnProfileNeedsConsent() throws Exception {
-        final PlatformVpnProfile profile = getPlatformVpnProfile();
-        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
-                .thenReturn(false);
-
-        // Expect intent to be returned, as consent has not already been granted.
-        final Intent intent = mVpnManager.provisionVpnProfile(profile);
-        assertNotNull(intent);
-
-        final ComponentName expectedComponentName =
-                ComponentName.unflattenFromString(
-                        "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog");
-        assertEquals(expectedComponentName, intent.getComponent());
-        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
-    }
-
-    @Test
-    public void testDeleteProvisionedVpnProfile() throws Exception {
-        mVpnManager.deleteProvisionedVpnProfile();
-        verify(mMockService).deleteVpnProfile(eq(PKG_NAME));
-    }
-
-    @Test
-    public void testStartProvisionedVpnProfile() throws Exception {
-        mVpnManager.startProvisionedVpnProfile();
-        verify(mMockService).startVpnProfile(eq(PKG_NAME));
-    }
-
-    @Test
-    public void testStopProvisionedVpnProfile() throws Exception {
-        mVpnManager.stopProvisionedVpnProfile();
-        verify(mMockService).stopVpnProfile(eq(PKG_NAME));
-    }
-
-    private Ikev2VpnProfile getPlatformVpnProfile() throws Exception {
-        return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
-                .setBypassable(true)
-                .setMaxMtu(1300)
-                .setMetered(true)
-                .setAuthPsk(PSK_BYTES)
-                .build();
-    }
-
-    @Test
-    public void testVpnTypesEqual() throws Exception {
-        SparseArray<String> vmVpnTypes = MessageUtils.findMessageNames(
-                new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" });
-        SparseArray<String> nativeVpnType = MessageUtils.findMessageNames(
-                new Class[] { NativeVpnType.class }, new String[]{ "" });
-
-        // TYPE_VPN_NONE = -1 is only defined in VpnManager.
-        assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size());
-        for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) {
-            assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i));
-        }
-    }
-}
diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
index 332f2a3..c491f37 100644
--- a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -54,17 +54,17 @@
         assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
     }
 
-@Test
-fun testBuilder_setNullTtl_success() {
-    val info = NsdServiceInfo().apply {
-        serviceType = "_ipp._tcp"
-    }
-    val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
-            .setTtl(null)
-            .build()
+    @Test
+    fun testBuilder_setNullTtl_success() {
+        val info = NsdServiceInfo().apply {
+            serviceType = "_ipp._tcp"
+        }
+        val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setTtl(null)
+                .build()
 
-    assertNull(request.ttl)
-}
+        assertNull(request.ttl)
+    }
 
     @Test
     fun testBuilder_setPropertiesSuccess() {
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 76a649e..9c812a1 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -16,6 +16,11 @@
 
 package android.net.nsd;
 
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.nsd.NsdManager.checkServiceInfoForRegistration;
+
+import static com.android.net.module.util.HexDump.hexStringToByteArray;
+
 import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
@@ -54,6 +59,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.net.InetAddress;
+import java.util.Collections;
 import java.util.List;
 import java.time.Duration;
 
@@ -240,7 +246,7 @@
 
         AdvertisingRequest capturedRequest = getAdvertisingRequest(
                 req -> verify(mServiceConn).registerService(anyInt(), req.capture()));
-        assertEquals(request, capturedRequest);
+        assertEquals(request.getTtl(), capturedRequest.getTtl());
     }
 
     private void doTestRegisterService() throws Exception {
@@ -395,6 +401,7 @@
         NsdManager.RegistrationListener listener4 = mock(NsdManager.RegistrationListener.class);
         NsdManager.RegistrationListener listener5 = mock(NsdManager.RegistrationListener.class);
         NsdManager.RegistrationListener listener6 = mock(NsdManager.RegistrationListener.class);
+        NsdManager.RegistrationListener listener7 = mock(NsdManager.RegistrationListener.class);
 
         NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
         NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
@@ -439,6 +446,19 @@
         validServiceWithCustomHostNoAddresses.setPort(2222);
         validServiceWithCustomHostNoAddresses.setHostname("a_host");
 
+        NsdServiceInfo validServiceWithPublicKey = new NsdServiceInfo("a_name", "_a_type._tcp");
+        validServiceWithPublicKey.setPublicKey(
+                hexStringToByteArray(
+                        "0201030dc141d0637960b98cbc12cfca"
+                                + "221d2879dac26ee5b460e9007c992e19"
+                                + "02d897c391b03764d448f7d0c772fdb0"
+                                + "3b1d9d6d52ff8886769e8e2362513565"
+                                + "270962d3"));
+
+        NsdServiceInfo invalidServiceWithTooShortPublicKey =
+                new NsdServiceInfo("a_name", "_a_type._tcp");
+        invalidServiceWithTooShortPublicKey.setPublicKey(hexStringToByteArray("0201"));
+
         // Service registration
         //  - invalid arguments
         mustFail(() -> { manager.unregisterService(null); });
@@ -449,6 +469,8 @@
         mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
         mustFail(() -> {
             manager.registerService(invalidMissingHostnameWithAddresses, PROTOCOL, listener1); });
+        mustFail(() -> {
+            manager.registerService(invalidServiceWithTooShortPublicKey, PROTOCOL, listener1); });
         manager.registerService(validService, PROTOCOL, listener1);
         //  - update without subtype is not allowed
         mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
@@ -479,6 +501,9 @@
         //  - registering a service with a custom host with no addresses is valid
         manager.registerService(validServiceWithCustomHostNoAddresses, PROTOCOL, listener6);
         manager.unregisterService(listener6);
+        //  - registering a service with a public key is valid
+        manager.registerService(validServiceWithPublicKey, PROTOCOL, listener7);
+        manager.unregisterService(listener7);
 
         // Discover service
         //  - invalid arguments
@@ -506,6 +531,229 @@
         mustFail(() -> { manager.resolveService(validService, listener3); });
     }
 
+    private static final class NsdServiceInfoBuilder {
+        private static final String SERVICE_NAME = "TestService";
+        private static final String SERVICE_TYPE = "_testservice._tcp";
+        private static final int SERVICE_PORT = 12345;
+        private static final String HOSTNAME = "TestHost";
+        private static final List<InetAddress> HOST_ADDRESSES =
+                List.of(parseNumericAddress("192.168.2.23"), parseNumericAddress("2001:db8::3"));
+        private static final byte[] PUBLIC_KEY =
+                hexStringToByteArray(
+                        "0201030dc141d0637960b98cbc12cfca"
+                                + "221d2879dac26ee5b460e9007c992e19"
+                                + "02d897c391b03764d448f7d0c772fdb0"
+                                + "3b1d9d6d52ff8886769e8e2362513565"
+                                + "270962d3");
+
+        private final NsdServiceInfo mNsdServiceInfo = new NsdServiceInfo();
+
+        NsdServiceInfo build() {
+            return mNsdServiceInfo;
+        }
+
+        NsdServiceInfoBuilder setNoService() {
+            mNsdServiceInfo.setServiceName(null);
+            mNsdServiceInfo.setServiceType(null);
+            mNsdServiceInfo.setPort(0);
+            return this;
+        }
+
+        NsdServiceInfoBuilder setService() {
+            mNsdServiceInfo.setServiceName(SERVICE_NAME);
+            mNsdServiceInfo.setServiceType(SERVICE_TYPE);
+            mNsdServiceInfo.setPort(SERVICE_PORT);
+            return this;
+        }
+
+        NsdServiceInfoBuilder setZeroPortService() {
+            mNsdServiceInfo.setServiceName(SERVICE_NAME);
+            mNsdServiceInfo.setServiceType(SERVICE_TYPE);
+            mNsdServiceInfo.setPort(0);
+            return this;
+        }
+
+        NsdServiceInfoBuilder setInvalidService() {
+            mNsdServiceInfo.setServiceName(SERVICE_NAME);
+            mNsdServiceInfo.setServiceType(null);
+            mNsdServiceInfo.setPort(SERVICE_PORT);
+            return this;
+        }
+
+        NsdServiceInfoBuilder setDefaultHost() {
+            mNsdServiceInfo.setHostname(null);
+            mNsdServiceInfo.setHostAddresses(Collections.emptyList());
+            return this;
+        }
+
+        NsdServiceInfoBuilder setCustomHost() {
+            mNsdServiceInfo.setHostname(HOSTNAME);
+            mNsdServiceInfo.setHostAddresses(HOST_ADDRESSES);
+            return this;
+        }
+
+        NsdServiceInfoBuilder setCustomHostNoAddress() {
+            mNsdServiceInfo.setHostname(HOSTNAME);
+            mNsdServiceInfo.setHostAddresses(Collections.emptyList());
+            return this;
+        }
+
+        NsdServiceInfoBuilder setHostAddressesNoHostname() {
+            mNsdServiceInfo.setHostname(null);
+            mNsdServiceInfo.setHostAddresses(HOST_ADDRESSES);
+            return this;
+        }
+
+        NsdServiceInfoBuilder setNoPublicKey() {
+            mNsdServiceInfo.setPublicKey(null);
+            return this;
+        }
+
+        NsdServiceInfoBuilder setPublicKey() {
+            mNsdServiceInfo.setPublicKey(PUBLIC_KEY);
+            return this;
+        }
+
+        NsdServiceInfoBuilder setInvalidPublicKey() {
+            mNsdServiceInfo.setPublicKey(new byte[3]);
+            return this;
+        }
+    }
+
+    @Test
+    public void testCheckServiceInfoForRegistration() {
+        // The service is invalid
+        mustFail(() -> checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setInvalidService()
+                        .setCustomHost()
+                        .setPublicKey().build()));
+        // Keep compatible with the legacy behavior: It's allowed to set host
+        // addresses for a service registration although the host addresses
+        // won't be registered. To register the addresses for a host, the
+        // hostname must be specified.
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setService()
+                        .setHostAddressesNoHostname()
+                        .setPublicKey().build());
+        // The public key is invalid
+        mustFail(() -> checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setService()
+                        .setCustomHost()
+                        .setInvalidPublicKey().build()));
+        // Invalid combinations
+        // 1. (service, custom host, key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setService()
+                        .setCustomHost()
+                        .setPublicKey().build());
+        // 2. (service, custom host, no key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setService()
+                        .setCustomHost()
+                        .setNoPublicKey().build());
+        // 3. (service, no-address custom host, key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setService()
+                        .setCustomHostNoAddress()
+                        .setPublicKey().build());
+        // 4. (service, no-address custom host, no key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setService()
+                        .setCustomHostNoAddress()
+                        .setNoPublicKey().build());
+        // 5. (service, default host, key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setService()
+                        .setDefaultHost()
+                        .setPublicKey().build());
+        // 6. (service, default host, no key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setService()
+                        .setDefaultHost()
+                        .setNoPublicKey().build());
+        // 7. (0-port service, custom host, valid key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setZeroPortService()
+                        .setCustomHost()
+                        .setPublicKey().build());
+        // 8. (0-port service, custom host, no key): invalid
+        mustFail(() -> checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setZeroPortService()
+                        .setCustomHost()
+                        .setNoPublicKey().build()));
+        // 9. (0-port service, no-address custom host, key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setZeroPortService()
+                        .setCustomHostNoAddress()
+                        .setPublicKey().build());
+        // 10. (0-port service, no-address custom host, no key): invalid
+        mustFail(() -> checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setZeroPortService()
+                        .setCustomHostNoAddress()
+                        .setNoPublicKey().build()));
+        // 11. (0-port service, default host, key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setZeroPortService()
+                        .setDefaultHost()
+                        .setPublicKey().build());
+        // 12. (0-port service, default host, no key): invalid
+        mustFail(() -> checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setZeroPortService()
+                        .setDefaultHost()
+                        .setNoPublicKey().build()));
+        // 13. (no service, custom host, key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setNoService()
+                        .setCustomHost()
+                        .setPublicKey().build());
+        // 14. (no service, custom host, no key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setNoService()
+                        .setCustomHost()
+                        .setNoPublicKey().build());
+        // 15. (no service, no-address custom host, key): valid
+        checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setNoService()
+                        .setCustomHostNoAddress()
+                        .setPublicKey().build());
+        // 16. (no service, no-address custom host, no key): invalid
+        mustFail(() -> checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setNoService()
+                        .setCustomHostNoAddress()
+                        .setNoPublicKey().build()));
+        // 17. (no service, default host, key): invalid
+        mustFail(() -> checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setNoService()
+                        .setDefaultHost()
+                        .setPublicKey().build()));
+        // 18. (no service, default host, no key): invalid
+        mustFail(() -> checkServiceInfoForRegistration(
+                new NsdServiceInfoBuilder()
+                        .setNoService()
+                        .setDefaultHost()
+                        .setNoPublicKey().build()));
+    }
+
     public void mustFail(Runnable fn) {
         try {
             fn.run();
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt
new file mode 100644
index 0000000..8f86f06
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for {@link NsdServiceInfo}. */
+@SmallTest
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class NsdServiceInfoTest {
+    @Test
+    fun testToString_txtRecord() {
+        val info = NsdServiceInfo().apply {
+            this.setAttribute("abc", byteArrayOf(0xff.toByte(), 0xfe.toByte()))
+            this.setAttribute("def", null as String?)
+            this.setAttribute("ghi", "猫")
+            this.setAttribute("jkl", byteArrayOf(0, 0x21))
+            this.setAttribute("mno", "Hey Tom! It's you?.~{}")
+        }
+
+        val infoStr = info.toString()
+
+        assertTrue(
+            infoStr.contains("txtRecord: " +
+                "{abc=0xFFFE, def=(null), ghi=0xE78CAB, jkl=0x0021, mno=Hey Tom! It's you?.~{}}"),
+            infoStr)
+    }
+}
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
deleted file mode 100644
index acae7d2..0000000
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * 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.internal.net;
-
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
-
-import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
-import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
-import static com.android.testutils.ParcelUtils.assertParcelSane;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.net.IpSecAlgorithm;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/** Unit tests for {@link VpnProfile}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class VpnProfileTest {
-    private static final String DUMMY_PROFILE_KEY = "Test";
-
-    private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23;
-    private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24;
-    private static final int ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE = 25;
-    private static final int ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION = 26;
-    private static final int ENCODED_INDEX_IKE_TUN_CONN_PARAMS = 27;
-    private static final int ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED = 28;
-    private static final int ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED = 29;
-
-    @Test
-    public void testDefaults() throws Exception {
-        final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);
-
-        assertEquals(DUMMY_PROFILE_KEY, p.key);
-        assertEquals("", p.name);
-        assertEquals(VpnProfile.TYPE_PPTP, p.type);
-        assertEquals("", p.server);
-        assertEquals("", p.username);
-        assertEquals("", p.password);
-        assertEquals("", p.dnsServers);
-        assertEquals("", p.searchDomains);
-        assertEquals("", p.routes);
-        assertTrue(p.mppe);
-        assertEquals("", p.l2tpSecret);
-        assertEquals("", p.ipsecIdentifier);
-        assertEquals("", p.ipsecSecret);
-        assertEquals("", p.ipsecUserCert);
-        assertEquals("", p.ipsecCaCert);
-        assertEquals("", p.ipsecServerCert);
-        assertEquals(null, p.proxy);
-        assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty());
-        assertFalse(p.isBypassable);
-        assertFalse(p.isMetered);
-        assertEquals(1360, p.maxMtu);
-        assertFalse(p.areAuthParamsInline);
-        assertFalse(p.isRestrictedToTestNetworks);
-        assertFalse(p.excludeLocalRoutes);
-        assertFalse(p.requiresInternetValidation);
-        assertFalse(p.automaticNattKeepaliveTimerEnabled);
-        assertFalse(p.automaticIpVersionSelectionEnabled);
-    }
-
-    private VpnProfile getSampleIkev2Profile(String key) {
-        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
-                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
-                null /* ikeTunConnParams */, true /* mAutomaticNattKeepaliveTimerEnabled */,
-                true /* automaticIpVersionSelectionEnabled */);
-
-        p.name = "foo";
-        p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
-        p.server = "bar";
-        p.username = "baz";
-        p.password = "qux";
-        p.dnsServers = "8.8.8.8";
-        p.searchDomains = "";
-        p.routes = "0.0.0.0/0";
-        p.mppe = false;
-        p.l2tpSecret = "";
-        p.ipsecIdentifier = "quux";
-        p.ipsecSecret = "quuz";
-        p.ipsecUserCert = "corge";
-        p.ipsecCaCert = "grault";
-        p.ipsecServerCert = "garply";
-        p.proxy = null;
-        p.setAllowedAlgorithms(
-                Arrays.asList(
-                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
-                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
-                        IpSecAlgorithm.AUTH_HMAC_SHA512,
-                        IpSecAlgorithm.CRYPT_AES_CBC));
-        p.isBypassable = true;
-        p.isMetered = true;
-        p.maxMtu = 1350;
-        p.areAuthParamsInline = true;
-
-        // Not saved, but also not compared.
-        p.saveLogin = true;
-
-        return p;
-    }
-
-    private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
-        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
-                false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
-                new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS),
-                true /* mAutomaticNattKeepaliveTimerEnabled */,
-                true /* automaticIpVersionSelectionEnabled */);
-
-        p.name = "foo";
-        p.server = "bar";
-        p.dnsServers = "8.8.8.8";
-        p.searchDomains = "";
-        p.routes = "0.0.0.0/0";
-        p.mppe = false;
-        p.proxy = null;
-        p.setAllowedAlgorithms(
-                Arrays.asList(
-                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
-                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
-                        IpSecAlgorithm.AUTH_HMAC_SHA512,
-                        IpSecAlgorithm.CRYPT_AES_CBC));
-        p.isBypassable = true;
-        p.isMetered = true;
-        p.maxMtu = 1350;
-        p.areAuthParamsInline = true;
-
-        // Not saved, but also not compared.
-        p.saveLogin = true;
-
-        return p;
-    }
-
-    @Test
-    public void testEquals() {
-        assertEquals(
-                getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY));
-
-        final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        modified.maxMtu--;
-        assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified);
-    }
-
-    @Test
-    public void testParcelUnparcel() {
-        if (isAtLeastU()) {
-            // automaticNattKeepaliveTimerEnabled, automaticIpVersionSelectionEnabled added in U.
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28);
-            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28);
-        } else if (isAtLeastT()) {
-            // excludeLocalRoutes, requiresPlatformValidation were added in T.
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 26);
-            assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 26);
-        } else {
-            assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
-        }
-    }
-
-    @Test
-    public void testEncodeDecodeWithIkeTunConnParams() {
-        final VpnProfile profile = getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY);
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
-        assertEquals(profile, decoded);
-    }
-
-    @Test
-    public void testEncodeDecode() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
-        assertEquals(profile, decoded);
-    }
-
-    @Test
-    public void testEncodeDecodeTooManyValues() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        final byte[] tooManyValues =
-                (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes();
-
-        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
-    }
-
-    private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) {
-        // Sort to ensure when we remove, we can do it from greatest first.
-        Arrays.sort(missingIndices);
-
-        final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode());
-        final List<String> parts =
-                new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER)));
-
-        // Remove from back first to ensure indexing is consistent.
-        for (int i = missingIndices.length - 1; i >= 0; i--) {
-            parts.remove(missingIndices[i]);
-        }
-
-        return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0]));
-    }
-
-    @Test
-    public void testEncodeDecodeInvalidNumberOfValues() {
-        final String tooFewValues =
-                getEncodedDecodedIkev2ProfileMissingValues(
-                        ENCODED_INDEX_AUTH_PARAMS_INLINE,
-                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
-                        ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
-                        ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
-                        ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
-                        ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
-                        ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED
-                        /* missingIndices */);
-
-        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
-    }
-
-    private String getEncodedDecodedIkev2ProfileWithtooFewValues() {
-        return getEncodedDecodedIkev2ProfileMissingValues(
-                ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
-                ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
-                ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
-                ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
-                ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
-                ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED /* missingIndices */);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingIsRestrictedToTestNetworks() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without isRestrictedToTestNetworks defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.isRestrictedToTestNetworks);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingExcludeLocalRoutes() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without excludeLocalRoutes defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.excludeLocalRoutes);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingRequiresValidation() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without requiresValidation defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.requiresInternetValidation);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingAutomaticNattKeepaliveTimerEnabled() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without automaticNattKeepaliveTimerEnabled defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.automaticNattKeepaliveTimerEnabled);
-    }
-
-    @Test
-    public void testEncodeDecodeMissingAutomaticIpVersionSelectionEnabled() {
-        final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
-        // Verify decoding without automaticIpVersionSelectionEnabled defaults to false
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
-        assertFalse(decoded.automaticIpVersionSelectionEnabled);
-    }
-
-    @Test
-    public void testEncodeDecodeLoginsNotSaved() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        profile.saveLogin = false;
-
-        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
-        assertNotEquals(profile, decoded);
-
-        // Add the username/password back, everything else must be equal.
-        decoded.username = profile.username;
-        decoded.password = profile.password;
-        assertEquals(profile, decoded);
-    }
-
-    @Test
-    public void testClone() {
-        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
-        final VpnProfile clone = profile.clone();
-        assertEquals(profile, clone);
-        assertNotSame(profile, clone);
-    }
-}
diff --git a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
index 53baee1..8a9286f 100644
--- a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
@@ -1,5 +1,6 @@
 package com.android.metrics
 
+import android.net.ConnectivityThread
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.CONNECTIVITY_MANAGED_CAPABILITIES
 import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
@@ -15,13 +16,20 @@
 import android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
 import android.net.NetworkScore
 import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
 import android.net.NetworkScore.POLICY_EXITING
 import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
 import android.os.Build
 import android.os.Handler
+import android.os.Process
+import android.os.Process.SYSTEM_UID
 import android.stats.connectivity.MeteredState
+import android.stats.connectivity.RequestType
+import android.stats.connectivity.RequestType.RT_APP
+import android.stats.connectivity.RequestType.RT_SYSTEM
+import android.stats.connectivity.RequestType.RT_SYSTEM_ON_BEHALF_OF_APP
 import android.stats.connectivity.ValidatedState
 import androidx.test.filters.SmallTest
 import com.android.net.module.util.BitUtils
@@ -31,11 +39,13 @@
 import com.android.server.connectivity.FullScore.POLICY_IS_UNMETERED
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
+import com.android.testutils.TestableNetworkCallback
 import java.util.concurrent.CompletableFuture
 import kotlin.test.assertEquals
 import kotlin.test.fail
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
 
 private fun <T> Handler.onHandler(f: () -> T): T {
     val future = CompletableFuture<T>()
@@ -80,7 +90,7 @@
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
 class ConnectivitySampleMetricsTest : CSTest() {
     @Test
-    fun testSampleConnectivityState() {
+    fun testSampleConnectivityState_Network() {
         val wifi1Caps = NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_WIFI)
                 .addCapability(NET_CAPABILITY_NOT_METERED)
@@ -179,4 +189,61 @@
                         "expected ${expectedWifi2Policies.toPolicyString()}, " +
                         "found ${foundWifi2.scorePolicies.toPolicyString()}")
     }
+
+    private fun fileNetworkRequest(requestType: RequestType, requestCount: Int, uid: Int? = null) {
+        if (uid != null) {
+            deps.setCallingUid(uid)
+        }
+        try {
+            repeat(requestCount) {
+                when (requestType) {
+                    RT_APP, RT_SYSTEM -> cm.requestNetwork(
+                            NetworkRequest.Builder().build(),
+                            TestableNetworkCallback()
+                    )
+
+                    RT_SYSTEM_ON_BEHALF_OF_APP -> cm.registerDefaultNetworkCallbackForUid(
+                            Process.myUid(),
+                            TestableNetworkCallback(),
+                            Handler(ConnectivityThread.getInstanceLooper()))
+
+                    else -> fail("invalid requestType: " + requestType)
+                }
+            }
+        } finally {
+            deps.unmockCallingUid()
+        }
+    }
+
+
+    @Test
+    fun testSampleConnectivityState_NetworkRequest() {
+        val requestCount = 5
+        fileNetworkRequest(RT_APP, requestCount);
+        fileNetworkRequest(RT_SYSTEM, requestCount, SYSTEM_UID);
+        fileNetworkRequest(RT_SYSTEM_ON_BEHALF_OF_APP, requestCount, SYSTEM_UID);
+
+        val stats = csHandler.onHandler { service.sampleConnectivityState() }
+
+        assertEquals(3, stats.networkRequestCount.requestCountForTypeList.size)
+        val appRequest = stats.networkRequestCount.requestCountForTypeList.find {
+            it.requestType == RT_APP
+        } ?: fail("Can't find RT_APP request")
+        val systemRequest = stats.networkRequestCount.requestCountForTypeList.find {
+            it.requestType == RT_SYSTEM
+        } ?: fail("Can't find RT_SYSTEM request")
+        val systemOnBehalfOfAppRequest = stats.networkRequestCount.requestCountForTypeList.find {
+            it.requestType == RT_SYSTEM_ON_BEHALF_OF_APP
+        } ?: fail("Can't find RT_SYSTEM_ON_BEHALF_OF_APP request")
+
+        // Verify request count is equal or larger than the number of request this test filed
+        // since ConnectivityService internally files network requests
+        assertTrue("Unexpected RT_APP count, expected >= $requestCount, " +
+                "found ${appRequest.requestCount}", appRequest.requestCount >= requestCount)
+        assertTrue("Unexpected RT_SYSTEM count, expected >= $requestCount, " +
+                "found ${systemRequest.requestCount}", systemRequest.requestCount >= requestCount)
+        assertTrue("Unexpected RT_SYSTEM_ON_BEHALF_OF_APP count, expected >= $requestCount, " +
+                "found ${systemOnBehalfOfAppRequest.requestCount}",
+                systemOnBehalfOfAppRequest.requestCount >= requestCount)
+    }
 }
diff --git a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
index 3f6e88d..10ba6a4 100644
--- a/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/NetworkNsdReportedMetricsTest.kt
@@ -163,7 +163,8 @@
         val sentQueryCount = 150
         val metrics = NetworkNsdReportedMetrics(clientId, deps)
         metrics.reportServiceDiscoveryStop(true /* isLegacy */, transactionId, durationMs,
-                foundCallbackCount, lostCallbackCount, servicesCount, sentQueryCount)
+                foundCallbackCount, lostCallbackCount, servicesCount, sentQueryCount,
+                true /* isServiceFromCache */)
 
         val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
         verify(deps).statsWrite(eventCaptor.capture())
@@ -179,6 +180,7 @@
             assertEquals(servicesCount, it.foundServiceCount)
             assertEquals(durationMs, it.eventDurationMillisec)
             assertEquals(sentQueryCount, it.sentQueryCount)
+            assertTrue(it.isKnownService)
         }
     }
 
@@ -231,8 +233,10 @@
         val clientId = 99
         val transactionId = 100
         val durationMs = 10L
+        val sentQueryCount = 10
         val metrics = NetworkNsdReportedMetrics(clientId, deps)
-        metrics.reportServiceResolutionStop(true /* isLegacy */, transactionId, durationMs)
+        metrics.reportServiceResolutionStop(
+                true /* isLegacy */, transactionId, durationMs, sentQueryCount)
 
         val eventCaptor = ArgumentCaptor.forClass(NetworkNsdReported::class.java)
         verify(deps).statsWrite(eventCaptor.capture())
@@ -243,6 +247,7 @@
             assertEquals(NsdEventType.NET_RESOLVE, it.type)
             assertEquals(MdnsQueryResult.MQR_SERVICE_RESOLUTION_STOP, it.queryResult)
             assertEquals(durationMs, it.eventDurationMillisec)
+            assertEquals(sentQueryCount, it.sentQueryCount)
         }
     }
 
diff --git a/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt b/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt
deleted file mode 100644
index 2cf6b17..0000000
--- a/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.server
-
-import android.os.Build
-import androidx.test.filters.SmallTest
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
-import com.android.testutils.DevSdkIgnoreRunner
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@SmallTest
-@IgnoreUpTo(Build.VERSION_CODES.S)
-class BpfLoaderRcUtilsTest {
-    @Test
-    fun testLoadExistingBpfRcFile() {
-
-        val inputString = """
-            service a
-            # test comment
-            service bpfloader /system/bin/bpfloader
-                capabilities CHOWN SYS_ADMIN NET_ADMIN
-                group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
-                user root
-                rlimit memlock 1073741824 1073741824
-                oneshot
-                # comment 漢字
-                reboot_on_failure reboot,bpfloader-failed
-                updatable
-            
-            #test comment
-            on b 
-              oneshot 
-              # test comment
-        """.trimIndent()
-        val expectedResult = listOf(
-            "service bpfloader /system/bin/bpfloader",
-            "capabilities CHOWN SYS_ADMIN NET_ADMIN",
-            "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
-            "user root",
-            "rlimit memlock 1073741824 1073741824",
-            "oneshot",
-            "reboot_on_failure reboot,bpfloader-failed",
-            "updatable"
-        )
-
-        assertEquals(expectedResult,
-                BpfLoaderRcUtils.loadExistingBpfRcFile(inputString.byteInputStream()))
-    }
-
-    @Test
-    fun testCheckBpfRcFile() {
-        assertTrue(BpfLoaderRcUtils.checkBpfLoaderRc())
-    }
-}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index ea905d5..c1c15ca 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -17,6 +17,7 @@
 package com.android.server;
 
 import static android.net.BpfNetMapsConstants.ALLOW_CHAINS;
+import static android.net.BpfNetMapsConstants.BACKGROUND_MATCH;
 import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
 import static android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED;
@@ -31,13 +32,26 @@
 import static android.net.BpfNetMapsConstants.OEM_DENY_1_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_2_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_3_MATCH;
-import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_ADMIN_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_USER_MATCH;
 import static android.net.BpfNetMapsConstants.POWERSAVE_MATCH;
 import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
 import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
 import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISABLED;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
+import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.BLOCKED_REASON_OEM_DENY;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
@@ -74,7 +88,9 @@
 import android.net.InetAddresses;
 import android.net.UidOwnerValue;
 import android.os.Build;
+import android.os.Process;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
@@ -121,6 +137,12 @@
 
     private static final int TEST_UID = 10086;
     private static final int[] TEST_UIDS = {10002, 10003};
+    private static final int[] CORE_AIDS = {
+            Process.ROOT_UID,
+            Process.SYSTEM_UID,
+            Process.FIRST_APPLICATION_UID - 10,
+            Process.FIRST_APPLICATION_UID - 1,
+    };
     private static final String TEST_IF_NAME = "wlan0";
     private static final int TEST_IF_INDEX = 7;
     private static final int NO_IIF = 0;
@@ -334,146 +356,6 @@
         }
     }
 
-    private void doTestRemoveNaughtyApp(final int iif, final long match) throws Exception {
-        mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(iif, match));
-
-        mBpfNetMaps.removeNaughtyApp(TEST_UID);
-
-        checkUidOwnerValue(TEST_UID, iif, match & ~PENALTY_BOX_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testRemoveNaughtyApp() throws Exception {
-        doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH);
-
-        // PENALTY_BOX_MATCH with other matches
-        doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
-
-        // PENALTY_BOX_MATCH with IIF_MATCH
-        doTestRemoveNaughtyApp(TEST_IF_INDEX, PENALTY_BOX_MATCH | IIF_MATCH);
-
-        // PENALTY_BOX_MATCH is not enabled
-        doTestRemoveNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testRemoveNaughtyAppMissingUid() {
-        // UidOwnerMap does not have entry for TEST_UID
-        assertThrows(ServiceSpecificException.class,
-                () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
-    }
-
-    @Test
-    @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testRemoveNaughtyAppBeforeT() {
-        assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
-    }
-
-    private void doTestAddNaughtyApp(final int iif, final long match) throws Exception {
-        if (match != NO_MATCH) {
-            mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(iif, match));
-        }
-
-        mBpfNetMaps.addNaughtyApp(TEST_UID);
-
-        checkUidOwnerValue(TEST_UID, iif, match | PENALTY_BOX_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testAddNaughtyApp() throws Exception {
-        doTestAddNaughtyApp(NO_IIF, NO_MATCH);
-
-        // Other matches are enabled
-        doTestAddNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
-
-        // IIF_MATCH is enabled
-        doTestAddNaughtyApp(TEST_IF_INDEX, IIF_MATCH);
-
-        // PENALTY_BOX_MATCH is already enabled
-        doTestAddNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH);
-    }
-
-    @Test
-    @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testAddNaughtyAppBeforeT() {
-        assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.addNaughtyApp(TEST_UID));
-    }
-
-    private void doTestRemoveNiceApp(final int iif, final long match) throws Exception {
-        mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(iif, match));
-
-        mBpfNetMaps.removeNiceApp(TEST_UID);
-
-        checkUidOwnerValue(TEST_UID, iif, match & ~HAPPY_BOX_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testRemoveNiceApp() throws Exception {
-        doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH);
-
-        // HAPPY_BOX_MATCH with other matches
-        doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
-
-        // HAPPY_BOX_MATCH with IIF_MATCH
-        doTestRemoveNiceApp(TEST_IF_INDEX, HAPPY_BOX_MATCH | IIF_MATCH);
-
-        // HAPPY_BOX_MATCH is not enabled
-        doTestRemoveNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testRemoveNiceAppMissingUid() {
-        // UidOwnerMap does not have entry for TEST_UID
-        assertThrows(ServiceSpecificException.class,
-                () -> mBpfNetMaps.removeNiceApp(TEST_UID));
-    }
-
-    @Test
-    @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testRemoveNiceAppBeforeT() {
-        assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.removeNiceApp(TEST_UID));
-    }
-
-    private void doTestAddNiceApp(final int iif, final long match) throws Exception {
-        if (match != NO_MATCH) {
-            mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(iif, match));
-        }
-
-        mBpfNetMaps.addNiceApp(TEST_UID);
-
-        checkUidOwnerValue(TEST_UID, iif, match | HAPPY_BOX_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testAddNiceApp() throws Exception {
-        doTestAddNiceApp(NO_IIF, NO_MATCH);
-
-        // Other matches are enabled
-        doTestAddNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
-
-        // IIF_MATCH is enabled
-        doTestAddNiceApp(TEST_IF_INDEX, IIF_MATCH);
-
-        // HAPPY_BOX_MATCH is already enabled
-        doTestAddNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH);
-    }
-
-    @Test
-    @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testAddNiceAppBeforeT() {
-        assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.addNiceApp(TEST_UID));
-    }
-
     private void doTestUpdateUidLockdownRule(final int iif, final long match, final boolean add)
             throws Exception {
         if (match != NO_MATCH) {
@@ -658,6 +540,9 @@
         doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_1);
         doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_2);
         doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_3);
+        doTestSetUidRule(FIREWALL_CHAIN_METERED_ALLOW);
+        doTestSetUidRule(FIREWALL_CHAIN_METERED_DENY_USER);
+        doTestSetUidRule(FIREWALL_CHAIN_METERED_DENY_ADMIN);
     }
 
     @Test
@@ -972,6 +857,21 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testGetNetPermFoUid() throws Exception {
+        mUidPermissionMap.deleteEntry(new S32(TEST_UID));
+        assertEquals(PERMISSION_INTERNET, mBpfNetMaps.getNetPermForUid(TEST_UID));
+
+        mUidPermissionMap.updateEntry(new S32(TEST_UID), new U8((short) PERMISSION_NONE));
+        assertEquals(PERMISSION_NONE, mBpfNetMaps.getNetPermForUid(TEST_UID));
+
+        mUidPermissionMap.updateEntry(new S32(TEST_UID),
+                new U8((short) (PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS)));
+        assertEquals(PERMISSION_INTERNET | PERMISSION_UPDATE_DEVICE_STATS,
+                mBpfNetMaps.getNetPermForUid(TEST_UID));
+    }
+
+    @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testSwapActiveStatsMap() throws Exception {
         mConfigurationMap.updateEntry(
@@ -1079,7 +979,7 @@
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testDumpUidOwnerMap() throws Exception {
         doTestDumpUidOwnerMap(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH");
-        doTestDumpUidOwnerMap(PENALTY_BOX_MATCH, "PENALTY_BOX_MATCH");
+        doTestDumpUidOwnerMap(PENALTY_BOX_USER_MATCH, "PENALTY_BOX_USER_MATCH");
         doTestDumpUidOwnerMap(DOZABLE_MATCH, "DOZABLE_MATCH");
         doTestDumpUidOwnerMap(STANDBY_MATCH, "STANDBY_MATCH");
         doTestDumpUidOwnerMap(POWERSAVE_MATCH, "POWERSAVE_MATCH");
@@ -1089,6 +989,7 @@
         doTestDumpUidOwnerMap(OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH");
         doTestDumpUidOwnerMap(OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH");
         doTestDumpUidOwnerMap(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH");
+        doTestDumpUidOwnerMap(PENALTY_BOX_ADMIN_MATCH, "PENALTY_BOX_ADMIN_MATCH");
 
         doTestDumpUidOwnerMap(HAPPY_BOX_MATCH | POWERSAVE_MATCH,
                 "HAPPY_BOX_MATCH POWERSAVE_MATCH");
@@ -1137,7 +1038,6 @@
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testDumpUidOwnerMapConfig() throws Exception {
         doTestDumpOwnerMatchConfig(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH");
-        doTestDumpOwnerMatchConfig(PENALTY_BOX_MATCH, "PENALTY_BOX_MATCH");
         doTestDumpOwnerMatchConfig(DOZABLE_MATCH, "DOZABLE_MATCH");
         doTestDumpOwnerMatchConfig(STANDBY_MATCH, "STANDBY_MATCH");
         doTestDumpOwnerMatchConfig(POWERSAVE_MATCH, "POWERSAVE_MATCH");
@@ -1283,4 +1183,178 @@
         assertDumpContains(dump, TEST_V6_ADDRESS.getHostAddress());
         assertDumpContains(dump, TEST_IF_INDEX + "(" + TEST_IF_NAME + ")");
     }
+
+    private void doTestGetUidNetworkingBlockedReasons(
+            final long configurationMatches,
+            final long uidRules,
+            final short dataSaverStatus,
+            final int expectedBlockedReasons
+    ) throws Exception {
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(configurationMatches));
+        mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(NULL_IIF, uidRules));
+        mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(dataSaverStatus));
+
+        assertEquals(expectedBlockedReasons, mBpfNetMaps.getUidNetworkingBlockedReasons(TEST_UID));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testGetUidNetworkingBlockedReasons() throws Exception {
+        doTestGetUidNetworkingBlockedReasons(
+                NO_MATCH,
+                NO_MATCH,
+                DATA_SAVER_DISABLED,
+                BLOCKED_REASON_NONE
+        );
+        doTestGetUidNetworkingBlockedReasons(
+                DOZABLE_MATCH,
+                NO_MATCH,
+                DATA_SAVER_DISABLED,
+                BLOCKED_REASON_DOZE
+        );
+        doTestGetUidNetworkingBlockedReasons(
+                DOZABLE_MATCH | POWERSAVE_MATCH | STANDBY_MATCH,
+                DOZABLE_MATCH | STANDBY_MATCH,
+                DATA_SAVER_DISABLED,
+                BLOCKED_REASON_BATTERY_SAVER | BLOCKED_REASON_APP_STANDBY
+        );
+        doTestGetUidNetworkingBlockedReasons(
+                OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH,
+                OEM_DENY_1_MATCH | OEM_DENY_3_MATCH,
+                DATA_SAVER_DISABLED,
+                BLOCKED_REASON_OEM_DENY
+        );
+        doTestGetUidNetworkingBlockedReasons(
+                DOZABLE_MATCH,
+                DOZABLE_MATCH | BACKGROUND_MATCH | STANDBY_MATCH,
+                DATA_SAVER_DISABLED,
+                BLOCKED_REASON_NONE
+        );
+
+        // Note that HAPPY_BOX and PENALTY_BOX are not disabled by configuration map
+        doTestGetUidNetworkingBlockedReasons(
+                NO_MATCH,
+                PENALTY_BOX_USER_MATCH,
+                DATA_SAVER_DISABLED,
+                BLOCKED_METERED_REASON_USER_RESTRICTED
+        );
+        doTestGetUidNetworkingBlockedReasons(
+                NO_MATCH,
+                PENALTY_BOX_ADMIN_MATCH,
+                DATA_SAVER_ENABLED,
+                BLOCKED_METERED_REASON_ADMIN_DISABLED | BLOCKED_METERED_REASON_DATA_SAVER
+        );
+        doTestGetUidNetworkingBlockedReasons(
+                NO_MATCH,
+                PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH | HAPPY_BOX_MATCH,
+                DATA_SAVER_ENABLED,
+                BLOCKED_METERED_REASON_USER_RESTRICTED | BLOCKED_METERED_REASON_ADMIN_DISABLED
+        );
+        doTestGetUidNetworkingBlockedReasons(
+                STANDBY_MATCH,
+                STANDBY_MATCH | PENALTY_BOX_USER_MATCH | HAPPY_BOX_MATCH,
+                DATA_SAVER_ENABLED,
+                BLOCKED_REASON_APP_STANDBY | BLOCKED_METERED_REASON_USER_RESTRICTED
+        );
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testIsUidNetworkingBlockedForCoreUids() throws Exception {
+        final long allowlistMatch = BACKGROUND_MATCH;    // Enable any allowlist match.
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(allowlistMatch));
+
+        // Verify that a normal uid that is not on this chain is indeed blocked.
+        assertTrue(BpfNetMapsUtils.isUidNetworkingBlocked(TEST_UID, false, mConfigurationMap,
+                mUidOwnerMap, mDataSaverEnabledMap));
+
+        // Core appIds are not on the chain but should still be allowed on any user.
+        for (int userId = 0; userId < 20; userId++) {
+            for (final int aid : CORE_AIDS) {
+                final int uid = UserHandle.getUid(userId, aid);
+                assertFalse(BpfNetMapsUtils.isUidNetworkingBlocked(uid, false, mConfigurationMap,
+                        mUidOwnerMap, mDataSaverEnabledMap));
+            }
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testGetUidNetworkingBlockedReasonsForCoreUids() throws Exception {
+        // Enable BACKGROUND_MATCH that is an allowlist match.
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(BACKGROUND_MATCH));
+
+        // Non-core uid that is not on this chain is blocked by BLOCKED_REASON_APP_BACKGROUND.
+        assertEquals(BLOCKED_REASON_APP_BACKGROUND, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+                TEST_UID, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+
+        // Core appIds are not on the chain but should not be blocked on any users.
+        for (int userId = 0; userId < 20; userId++) {
+            for (final int aid : CORE_AIDS) {
+                final int uid = UserHandle.getUid(userId, aid);
+                assertEquals(BLOCKED_REASON_NONE, BpfNetMapsUtils.getUidNetworkingBlockedReasons(
+                        uid, mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap));
+            }
+        }
+    }
+
+    private void doTestIsUidRestrictedOnMeteredNetworks(
+            final long enabledMatches,
+            final long uidRules,
+            final short dataSaver,
+            final boolean expectedRestricted
+    ) throws Exception {
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(enabledMatches));
+        mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(NULL_IIF, uidRules));
+        mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(dataSaver));
+
+        assertEquals(expectedRestricted, mBpfNetMaps.isUidRestrictedOnMeteredNetworks(TEST_UID));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
+        doTestIsUidRestrictedOnMeteredNetworks(
+                NO_MATCH,
+                NO_MATCH,
+                DATA_SAVER_DISABLED,
+                false /* expectRestricted */
+        );
+        doTestIsUidRestrictedOnMeteredNetworks(
+                DOZABLE_MATCH | POWERSAVE_MATCH | STANDBY_MATCH,
+                DOZABLE_MATCH | STANDBY_MATCH ,
+                DATA_SAVER_DISABLED,
+                false /* expectRestricted */
+        );
+        doTestIsUidRestrictedOnMeteredNetworks(
+                NO_MATCH,
+                PENALTY_BOX_USER_MATCH,
+                DATA_SAVER_DISABLED,
+                true /* expectRestricted */
+        );
+        doTestIsUidRestrictedOnMeteredNetworks(
+                NO_MATCH,
+                PENALTY_BOX_ADMIN_MATCH,
+                DATA_SAVER_DISABLED,
+                true /* expectRestricted */
+        );
+        doTestIsUidRestrictedOnMeteredNetworks(
+                NO_MATCH,
+                PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH | HAPPY_BOX_MATCH,
+                DATA_SAVER_DISABLED,
+                true /* expectRestricted */
+        );
+        doTestIsUidRestrictedOnMeteredNetworks(
+                NO_MATCH,
+                NO_MATCH,
+                DATA_SAVER_ENABLED,
+                true /* expectRestricted */
+        );
+        doTestIsUidRestrictedOnMeteredNetworks(
+                NO_MATCH,
+                HAPPY_BOX_MATCH,
+                DATA_SAVER_ENABLED,
+                false /* expectRestricted */
+        );
+    }
 }
diff --git a/tests/unit/java/com/android/server/CallbackQueueTest.kt b/tests/unit/java/com/android/server/CallbackQueueTest.kt
new file mode 100644
index 0000000..d8d35c1
--- /dev/null
+++ b/tests/unit/java/com/android/server/CallbackQueueTest.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.CALLBACK_AVAILABLE
+import android.net.ConnectivityManager.CALLBACK_CAP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_IP_CHANGED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_NETID_1 = 123
+
+// Maximum 16 bits unsigned value
+private const val TEST_NETID_2 = 0xffff
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CallbackQueueTest {
+    @Test
+    fun testAddCallback() {
+        val cbs = listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_2 to CALLBACK_AVAILABLE,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED
+        )
+        val queue = CallbackQueue(intArrayOf()).apply {
+            cbs.forEach { addCallback(it.first, it.second) }
+        }
+
+        assertQueueEquals(cbs, queue)
+    }
+
+    @Test
+    fun testHasCallback() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+        }
+
+        assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_AVAILABLE))
+        assertTrue(queue.hasCallback(TEST_NETID_2, CALLBACK_AVAILABLE))
+        assertTrue(queue.hasCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED))
+
+        assertFalse(queue.hasCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED))
+        assertFalse(queue.hasCallback(1234, CALLBACK_AVAILABLE))
+        assertFalse(queue.hasCallback(TEST_NETID_1, 5678))
+        assertFalse(queue.hasCallback(1234, 5678))
+    }
+
+    @Test
+    fun testRemoveCallbacks() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            assertFalse(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            assertTrue(removeCallbacks(TEST_NETID_1, CALLBACK_AVAILABLE))
+        }
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_2 to CALLBACK_AVAILABLE
+        ), queue)
+    }
+
+    @Test
+    fun testRemoveCallbacksForNetId() {
+        val queue = CallbackQueue(intArrayOf()).apply {
+            assertFalse(removeCallbacksForNetId(TEST_NETID_2))
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+            assertFalse(removeCallbacksForNetId(TEST_NETID_1))
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_1, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_IP_CHANGED)
+            assertTrue(removeCallbacksForNetId(TEST_NETID_2))
+        }
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_1 to CALLBACK_CAP_CHANGED,
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+        ), queue)
+    }
+
+    @Test
+    fun testConstructorFromExistingArray() {
+        val queue1 = CallbackQueue(intArrayOf()).apply {
+            addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            addCallback(TEST_NETID_2, CALLBACK_AVAILABLE)
+        }
+        val queue2 = CallbackQueue(queue1.minimizedBackingArray)
+        assertQueueEquals(listOf(
+            TEST_NETID_1 to CALLBACK_AVAILABLE,
+            TEST_NETID_2 to CALLBACK_AVAILABLE
+        ), queue2)
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals("[]", CallbackQueue(intArrayOf()).toString())
+        assertEquals(
+            "[CALLBACK_AVAILABLE($TEST_NETID_1)]",
+            CallbackQueue(intArrayOf()).apply {
+                addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+            }.toString()
+        )
+        assertEquals(
+            "[CALLBACK_AVAILABLE($TEST_NETID_1),CALLBACK_CAP_CHANGED($TEST_NETID_2)]",
+            CallbackQueue(intArrayOf()).apply {
+                addCallback(TEST_NETID_1, CALLBACK_AVAILABLE)
+                addCallback(TEST_NETID_2, CALLBACK_CAP_CHANGED)
+            }.toString()
+        )
+    }
+
+    @Test
+    fun testMaxNetId() {
+        // CallbackQueue assumes netIds are at most 16 bits
+        assertTrue(NetIdManager.MAX_NET_ID <= 0xffff)
+    }
+
+    @Test
+    fun testMaxCallbackId() {
+        // CallbackQueue assumes callback IDs are at most 16 bits.
+        val constants = ConnectivityManager::class.java.declaredFields.filter {
+            Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
+                    it.name.startsWith("CALLBACK_")
+        }
+        constants.forEach {
+            it.isAccessible = true
+            assertTrue(it.get(null) as Int <= 0xffff)
+        }
+    }
+}
+
+private fun assertQueueEquals(expected: List<Pair<Int, Int>>, actual: CallbackQueue) {
+    assertEquals(
+        expected.size,
+        actual.length(),
+        "Size mismatch between expected: $expected and actual: $actual"
+    )
+
+    var nextIndex = 0
+    actual.forEach { netId, cbId ->
+        val (expNetId, expCbId) = expected[nextIndex]
+        val msg = "$actual does not match $expected at index $nextIndex"
+        assertEquals(expNetId, netId, msg)
+        assertEquals(expCbId, cbId, msg)
+        nextIndex++
+    }
+    // Ensure forEach iterations and size are consistent
+    assertEquals(expected.size, nextIndex)
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index c534025..999d17d 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -53,6 +53,7 @@
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
 import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
 import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
 import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.EXTRA_DEVICE_TYPE;
@@ -63,6 +64,9 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
@@ -84,6 +88,7 @@
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_FALLBACK;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP;
@@ -148,6 +153,7 @@
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED;
 import static android.net.Proxy.PROXY_CHANGE_ACTION;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED;
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_REMOVED;
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
@@ -158,11 +164,9 @@
 import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
 
 import static com.android.server.ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK;
-import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
 import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
 import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
-import static com.android.server.ConnectivityService.LOG_BPF_RC;
 import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
 import static com.android.server.ConnectivityService.PREFERENCE_ORDER_OEM;
@@ -173,6 +177,9 @@
 import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
 import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
 import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
+import static com.android.server.connectivity.ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN;
+import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
+import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
 import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.ConcurrentUtils.await;
 import static com.android.testutils.ConcurrentUtils.durationOf;
@@ -368,7 +375,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.security.Credentials;
 import android.system.Os;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
@@ -389,10 +395,10 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.net.VpnConfig;
-import com.android.internal.net.VpnProfile;
 import com.android.internal.util.WakeupMessage;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
 import com.android.net.module.util.CollectionUtils;
@@ -424,7 +430,6 @@
 import com.android.server.connectivity.SatelliteAccessController;
 import com.android.server.connectivity.TcpKeepaliveController;
 import com.android.server.connectivity.UidRangeUtils;
-import com.android.server.connectivity.VpnProfileStore;
 import com.android.server.net.NetworkPinner;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
@@ -458,13 +463,13 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.lang.reflect.Method;
 import java.net.DatagramSocket;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -631,7 +636,6 @@
     @Mock TelephonyManager mTelephonyManager;
     @Mock EthernetManager mEthernetManager;
     @Mock NetworkPolicyManager mNetworkPolicyManager;
-    @Mock VpnProfileStore mVpnProfileStore;
     @Mock SystemConfigManager mSystemConfigManager;
     @Mock DevicePolicyManager mDevicePolicyManager;
     @Mock Resources mResources;
@@ -802,8 +806,10 @@
             // This relies on all contexts for a given user returning the same UM mock
             final DevicePolicyManager dpmMock = createContextAsUser(userHandle, 0 /* flags */)
                     .getSystemService(DevicePolicyManager.class);
-            doReturn(value).when(dpmMock).getDeviceOwner();
-            doReturn(value).when(mDevicePolicyManager).getDeviceOwner();
+            ComponentName componentName = value == null
+                    ? null : new ComponentName(value, "deviceOwnerClass");
+            doReturn(componentName).when(dpmMock).getDeviceOwnerComponentOnAnyUser();
+            doReturn(componentName).when(mDevicePolicyManager).getDeviceOwnerComponentOnAnyUser();
         }
 
         @Override
@@ -1667,23 +1673,11 @@
             waitForIdle();
         }
 
-        public void startLegacyVpnPrivileged(VpnProfile profile) {
-            switch (profile.type) {
-                case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
-                case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
-                case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
-                case VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS:
-                    startPlatformVpn();
-                    break;
-                case VpnProfile.TYPE_L2TP_IPSEC_PSK:
-                case VpnProfile.TYPE_L2TP_IPSEC_RSA:
-                case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
-                case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
-                case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
-                    startLegacyVpn();
-                    break;
-                default:
-                    fail("Unknown VPN profile type");
+        public void startLegacyVpnPrivileged(boolean isIkev2Vpn) {
+            if (isIkev2Vpn) {
+                startPlatformVpn();
+            } else {
+                startLegacyVpn();
             }
         }
 
@@ -1736,6 +1730,8 @@
     private void mockUidNetworkingBlocked() {
         doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
         ).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean());
+        doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
+        ).when(mBpfNetMaps).isUidNetworkingBlocked(anyInt(), anyBoolean());
     }
 
     private boolean isUidBlocked(int blockedReasons, boolean meteredNetwork) {
@@ -1751,7 +1747,15 @@
 
     private void setBlockedReasonChanged(int blockedReasons) {
         mBlockedReasons = blockedReasons;
-        mPolicyCallback.onUidBlockedReasonChanged(Process.myUid(), blockedReasons);
+        if (mDeps.isAtLeastV()) {
+            visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(),
+                    () -> mService.handleBlockedReasonsChanged(
+                            List.of(new Pair<>(Process.myUid(), blockedReasons))
+
+                    ));
+        } else {
+            mPolicyCallback.onUidBlockedReasonChanged(Process.myUid(), blockedReasons);
+        }
     }
 
     private Nat464Xlat getNat464Xlat(NetworkAgentWrapper mna) {
@@ -1932,11 +1936,16 @@
         mService.mLingerDelayMs = TEST_LINGER_DELAY_MS;
         mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS;
 
-        final ArgumentCaptor<NetworkPolicyCallback> policyCallbackCaptor =
-                ArgumentCaptor.forClass(NetworkPolicyCallback.class);
-        verify(mNetworkPolicyManager).registerNetworkPolicyCallback(any(),
-                policyCallbackCaptor.capture());
-        mPolicyCallback = policyCallbackCaptor.getValue();
+        if (mDeps.isAtLeastV()) {
+            verify(mNetworkPolicyManager, never()).registerNetworkPolicyCallback(any(), any());
+            mPolicyCallback = null;
+        } else {
+            final ArgumentCaptor<NetworkPolicyCallback> policyCallbackCaptor =
+                    ArgumentCaptor.forClass(NetworkPolicyCallback.class);
+            verify(mNetworkPolicyManager).registerNetworkPolicyCallback(any(),
+                    policyCallbackCaptor.capture());
+            mPolicyCallback = policyCallbackCaptor.getValue();
+        }
 
         // Create local CM before sending system ready so that we can answer
         // getSystemService() correctly.
@@ -1952,6 +1961,10 @@
         setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
         setAlwaysOnNetworks(false);
         setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
+
+        mDeps.setChangeIdEnabled(
+                true, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, Process.myUid());
+        doReturn(PERMISSION_INTERNET).when(mBpfNetMaps).getNetPermForUid(anyInt());
         // Note : Please do not add any new instrumentation here. If you need new instrumentation,
         // please add it in CSTest and use subclasses of CSTest instead of adding more
         // tools in ConnectivityServiceTest.
@@ -2055,12 +2068,16 @@
             };
         }
 
+        private BiConsumer<Integer, Integer> mCarrierPrivilegesLostListener;
+
         @Override
         public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
                 @NonNull final Context context,
                 @NonNull final TelephonyManager tm,
                 final boolean requestRestrictedWifiEnabled,
-                BiConsumer<Integer, Integer> listener) {
+                BiConsumer<Integer, Integer> listener,
+                @NonNull final Handler handler) {
+            mCarrierPrivilegesLostListener = listener;
             return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
         }
 
@@ -2161,13 +2178,11 @@
             switch (name) {
                 case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
                 case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
-                    return true;
                 case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
-                    return true;
+                case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
+                case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
                 case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
                     return true;
-                case DELAY_DESTROY_FROZEN_SOCKETS_VERSION:
-                    return true;
                 default:
                     return super.isFeatureEnabled(context, name);
             }
@@ -2178,10 +2193,14 @@
             switch (name) {
                 case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
                     return true;
-                case LOG_BPF_RC:
-                    return true;
                 case ALLOW_SATALLITE_NETWORK_FALLBACK:
                     return true;
+                case INGRESS_TO_VPN_ADDRESS_FILTERING:
+                    return true;
+                case BACKGROUND_FIREWALL_CHAIN:
+                    return true;
+                case DELAY_DESTROY_SOCKETS:
+                    return true;
                 default:
                     return super.isFeatureNotChickenedOut(context, name);
             }
@@ -2853,7 +2872,7 @@
         };
         final NetworkRequest request = mService.listenForNetwork(caps, messenger, binder,
                 NetworkCallback.FLAG_NONE, mContext.getOpPackageName(),
-                mContext.getAttributionTag());
+                mContext.getAttributionTag(), ~0 /* declaredMethodsFlag */);
         mService.releaseNetworkRequest(request);
         deathRecipient.get().binderDied();
         // Wait for the release message to be processed.
@@ -5389,7 +5408,7 @@
             mService.requestNetwork(Process.INVALID_UID, networkCapabilities,
                     NetworkRequest.Type.REQUEST.ordinal(), null, 0, null,
                     ConnectivityManager.TYPE_WIFI, NetworkCallback.FLAG_NONE,
-                    mContext.getPackageName(), getAttributionTag());
+                    mContext.getPackageName(), getAttributionTag(), ~0 /* declaredMethodsFlag */);
         });
 
         final NetworkRequest.Builder builder =
@@ -7522,13 +7541,13 @@
     @Test
     public void testNetworkCallbackMaximum() throws Exception {
         final int MAX_REQUESTS = 100;
-        final int CALLBACKS = 87;
+        final int CALLBACKS = 88;
         final int DIFF_INTENTS = 10;
         final int SAME_INTENTS = 10;
         final int SYSTEM_ONLY_MAX_REQUESTS = 250;
-        // Assert 1 (Default request filed before testing) + CALLBACKS + DIFF_INTENTS +
-        // 1 (same intent) = MAX_REQUESTS - 1, since the capacity is MAX_REQUEST - 1.
-        assertEquals(MAX_REQUESTS - 1, 1 + CALLBACKS + DIFF_INTENTS + 1);
+        // CALLBACKS + DIFF_INTENTS + 1 (same intent)
+        // = MAX_REQUESTS - 1, since the capacity is MAX_REQUEST - 1.
+        assertEquals(MAX_REQUESTS - 1, CALLBACKS + DIFF_INTENTS + 1);
 
         NetworkRequest networkRequest = new NetworkRequest.Builder().build();
         ArrayList<Object> registered = new ArrayList<>();
@@ -9862,6 +9881,28 @@
         assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
         assertExtraInfoFromCmPresent(mCellAgent);
 
+        // Remove PERMISSION_INTERNET and disable NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION
+        doReturn(INetd.PERMISSION_NONE).when(mBpfNetMaps).getNetPermForUid(Process.myUid());
+        mDeps.setChangeIdEnabled(false,
+                NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION, Process.myUid());
+
+        setBlockedReasonChanged(BLOCKED_REASON_DOZE);
+        if (mDeps.isAtLeastV()) {
+            // On V+, network access from app that does not have INTERNET permission is considered
+            // not blocked if NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION is disabled.
+            // So blocked status does not change from BLOCKED_REASON_NONE
+            cellNetworkCallback.assertNoCallback();
+            detailedCallback.assertNoCallback();
+        } else {
+            // On U-, onBlockedStatusChanged callback is called with blocked reasons CS receives
+            // from NPMS callback regardless of permission app has.
+            // Note that this cannot actually happen because on U-, NPMS will never notify any
+            // blocked reasons for apps that don't have the INTERNET permission.
+            cellNetworkCallback.expect(BLOCKED_STATUS, mCellAgent, cb -> cb.getBlocked());
+            detailedCallback.expect(BLOCKED_STATUS_INT, mCellAgent,
+                    cb -> cb.getReason() == BLOCKED_REASON_DOZE);
+        }
+
         mCm.unregisterNetworkCallback(cellNetworkCallback);
     }
 
@@ -10213,24 +10254,6 @@
         doAsUid(Process.SYSTEM_UID, () -> mCm.unregisterNetworkCallback(perUidCb));
     }
 
-    private VpnProfile setupLockdownVpn(int profileType) {
-        final String profileName = "testVpnProfile";
-        final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8);
-        doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
-
-        final VpnProfile profile = new VpnProfile(profileName);
-        profile.name = "My VPN";
-        profile.server = "192.0.2.1";
-        profile.dnsServers = "8.8.8.8";
-        profile.ipsecIdentifier = "My ipsecIdentifier";
-        profile.ipsecSecret = "My PSK";
-        profile.type = profileType;
-        final byte[] encodedProfile = profile.encode();
-        doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
-
-        return profile;
-    }
-
     private void establishLegacyLockdownVpn(Network underlying) throws Exception {
         // The legacy lockdown VPN only supports userId 0, and must have an underlying network.
         assertNotNull(underlying);
@@ -10242,7 +10265,7 @@
         mMockVpn.connect(true);
     }
 
-    private void doTestLockdownVpn(VpnProfile profile, boolean expectSetVpnDefaultForUids)
+    private void doTestLockdownVpn(boolean isIkev2Vpn)
             throws Exception {
         mServiceContext.setPermission(
                 Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
@@ -10280,8 +10303,8 @@
         b.expectBroadcast();
         // Simulate LockdownVpnTracker attempting to start the VPN since it received the
         // systemDefault callback.
-        mMockVpn.startLegacyVpnPrivileged(profile);
-        if (expectSetVpnDefaultForUids) {
+        mMockVpn.startLegacyVpnPrivileged(isIkev2Vpn);
+        if (isIkev2Vpn) {
             // setVpnDefaultForUids() releases the original network request and creates a VPN
             // request so LOST callback is received.
             defaultCallback.expect(LOST, mCellAgent);
@@ -10305,7 +10328,7 @@
         final NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
         b2.expectBroadcast();
         b3.expectBroadcast();
-        if (expectSetVpnDefaultForUids) {
+        if (isIkev2Vpn) {
             // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
             // network satisfier which has TYPE_VPN.
             assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10351,14 +10374,15 @@
         // callback with different network.
         final ExpectedBroadcast b6 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
         mMockVpn.stopVpnRunnerPrivileged();
-        mMockVpn.startLegacyVpnPrivileged(profile);
+
+        mMockVpn.startLegacyVpnPrivileged(isIkev2Vpn);
         // VPN network is disconnected (to restart)
         callback.expect(LOST, mMockVpn);
         defaultCallback.expect(LOST, mMockVpn);
         // The network preference is cleared when VPN is disconnected so it receives callbacks for
         // the system-wide default.
         defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiAgent);
-        if (expectSetVpnDefaultForUids) {
+        if (isIkev2Vpn) {
             // setVpnDefaultForUids() releases the original network request and creates a VPN
             // request so LOST callback is received.
             defaultCallback.expect(LOST, mWiFiAgent);
@@ -10367,7 +10391,7 @@
         b6.expectBroadcast();
 
         // While the VPN is reconnecting on the new network, everything is blocked.
-        if (expectSetVpnDefaultForUids) {
+        if (isIkev2Vpn) {
             // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
             // as the network satisfier.
             assertNull(mCm.getActiveNetworkInfo());
@@ -10388,7 +10412,7 @@
         systemDefaultCallback.assertNoCallback();
         b7.expectBroadcast();
         b8.expectBroadcast();
-        if (expectSetVpnDefaultForUids) {
+        if (isIkev2Vpn) {
             // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
             // network satisfier which has TYPE_VPN.
             assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10414,7 +10438,7 @@
         defaultCallback.assertNoCallback();
         systemDefaultCallback.assertNoCallback();
 
-        if (expectSetVpnDefaultForUids) {
+        if (isIkev2Vpn) {
             // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
             // network satisfier which has TYPE_VPN.
             assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10455,14 +10479,12 @@
 
     @Test
     public void testLockdownVpn_LegacyVpnRunner() throws Exception {
-        final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IPSEC_XAUTH_PSK);
-        doTestLockdownVpn(profile, false /* expectSetVpnDefaultForUids */);
+        doTestLockdownVpn(false /* isIkev2Vpn */);
     }
 
     @Test
     public void testLockdownVpn_Ikev2VpnRunner() throws Exception {
-        final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IKEV2_IPSEC_PSK);
-        doTestLockdownVpn(profile, true /* expectSetVpnDefaultForUids */);
+        doTestLockdownVpn(true /* isIkev2Vpn */);
     }
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -10518,24 +10540,33 @@
         doTestSetUidFirewallRule(FIREWALL_CHAIN_POWERSAVE, FIREWALL_RULE_DENY);
         doTestSetUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, FIREWALL_RULE_DENY);
         doTestSetUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, FIREWALL_RULE_DENY);
-        doTestSetUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, FIREWALL_RULE_DENY);
+        if (SdkLevel.isAtLeastV()) {
+            // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+            doTestSetUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, FIREWALL_RULE_DENY);
+        }
         doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_1, FIREWALL_RULE_ALLOW);
         doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_2, FIREWALL_RULE_ALLOW);
         doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_3, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_ADMIN, FIREWALL_RULE_ALLOW);
     }
 
     @Test @IgnoreUpTo(SC_V2)
     public void testSetFirewallChainEnabled() throws Exception {
-        final List<Integer> firewallChains = Arrays.asList(
+        final List<Integer> firewallChains = new ArrayList<>(Arrays.asList(
                 FIREWALL_CHAIN_DOZABLE,
                 FIREWALL_CHAIN_STANDBY,
                 FIREWALL_CHAIN_POWERSAVE,
                 FIREWALL_CHAIN_RESTRICTED,
                 FIREWALL_CHAIN_LOW_POWER_STANDBY,
-                FIREWALL_CHAIN_BACKGROUND,
                 FIREWALL_CHAIN_OEM_DENY_1,
                 FIREWALL_CHAIN_OEM_DENY_2,
-                FIREWALL_CHAIN_OEM_DENY_3);
+                FIREWALL_CHAIN_OEM_DENY_3));
+        if (SdkLevel.isAtLeastV()) {
+            // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+            firewallChains.add(FIREWALL_CHAIN_BACKGROUND);
+        }
         for (final int chain: firewallChains) {
             mCm.setFirewallChainEnabled(chain, true /* enabled */);
             verify(mBpfNetMaps).setChildChain(chain, true /* enable */);
@@ -10582,7 +10613,10 @@
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_POWERSAVE, allowlist);
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_RESTRICTED, allowlist);
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_LOW_POWER_STANDBY, allowlist);
-        doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_BACKGROUND, allowlist);
+        if (SdkLevel.isAtLeastV()) {
+            // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+            doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_BACKGROUND, allowlist);
+        }
 
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_STANDBY, denylist);
         doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_1, denylist);
@@ -10604,7 +10638,10 @@
         doTestReplaceFirewallChain(FIREWALL_CHAIN_POWERSAVE);
         doTestReplaceFirewallChain(FIREWALL_CHAIN_RESTRICTED);
         doTestReplaceFirewallChain(FIREWALL_CHAIN_LOW_POWER_STANDBY);
-        doTestReplaceFirewallChain(FIREWALL_CHAIN_BACKGROUND);
+        if (SdkLevel.isAtLeastV()) {
+            // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+            doTestReplaceFirewallChain(FIREWALL_CHAIN_BACKGROUND);
+        }
         doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_1);
         doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_2);
         doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_3);
@@ -13619,7 +13656,8 @@
                     IllegalArgumentException.class,
                     () -> mService.requestNetwork(Process.INVALID_UID, nc, reqTypeInt, null, 0,
                             null, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE,
-                            mContext.getPackageName(), getAttributionTag())
+                            mContext.getPackageName(), getAttributionTag(),
+                            ~0 /* declaredMethodsFlag */)
             );
         }
     }
@@ -17341,21 +17379,7 @@
     }
 
     @Test
-    public void testSubIdsClearedWithoutNetworkFactoryPermission() throws Exception {
-        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
-        final NetworkCapabilities nc = new NetworkCapabilities();
-        nc.setSubscriptionIds(Collections.singleton(Process.myUid()));
-
-        final NetworkCapabilities result =
-                mService.networkCapabilitiesRestrictedForCallerPermissions(
-                        nc, Process.myPid(), Process.myUid());
-        assertTrue(result.getSubscriptionIds().isEmpty());
-    }
-
-    @Test
-    public void testSubIdsExistWithNetworkFactoryPermission() throws Exception {
-        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
-
+    public void testSubIdsExist() throws Exception {
         final Set<Integer> subIds = Collections.singleton(Process.myUid());
         final NetworkCapabilities nc = new NetworkCapabilities();
         nc.setSubscriptionIds(subIds);
@@ -17381,8 +17405,7 @@
     }
 
     @Test
-    public void testNetworkRequestWithSubIdsWithNetworkFactoryPermission() throws Exception {
-        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+    public void testNetworkRequestWithSubIds() throws Exception {
         final PendingIntent pendingIntent = PendingIntent.getBroadcast(
                 mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
         final NetworkCallback networkCallback1 = new NetworkCallback();
@@ -17398,21 +17421,6 @@
     }
 
     @Test
-    public void testNetworkRequestWithSubIdsWithoutNetworkFactoryPermission() throws Exception {
-        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
-        final PendingIntent pendingIntent = PendingIntent.getBroadcast(
-                mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
-
-        final Class<SecurityException> expected = SecurityException.class;
-        assertThrows(
-                expected, () -> mCm.requestNetwork(getRequestWithSubIds(), new NetworkCallback()));
-        assertThrows(expected, () -> mCm.requestNetwork(getRequestWithSubIds(), pendingIntent));
-        assertThrows(
-                expected,
-                () -> mCm.registerNetworkCallback(getRequestWithSubIds(), new NetworkCallback()));
-    }
-
-    @Test
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testCarrierConfigAppSendNetworkRequestForRestrictedWifi() throws Exception {
         mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
@@ -17479,7 +17487,10 @@
                 .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
         doReturn(TEST_SUBSCRIPTION_ID).when(mCarrierPrivilegeAuthenticator)
                 .getSubIdFromNetworkCapabilities(any());
-        mService.onCarrierPrivilegesLost(lostPrivilegeUid, lostPrivilegeSubId);
+
+        visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(), () -> {
+            mDeps.mCarrierPrivilegesLostListener.accept(lostPrivilegeUid, lostPrivilegeSubId);
+        });
         waitForIdle();
 
         if (expectCapChanged) {
@@ -17493,11 +17504,12 @@
         }
 
         mWiFiAgent.disconnect();
-        waitForIdle();
 
         if (expectUnavailable) {
+            testFactory.expectRequestRemove();
             testFactory.assertRequestCountEquals(0);
         } else {
+            testFactory.expectRequestAdd();
             testFactory.assertRequestCountEquals(1);
         }
 
@@ -17547,6 +17559,47 @@
                 false /* expectUnavailable */,
                 true /* expectCapChanged */);
     }
+
+    @Test
+    public void testAllowedUidsExistWithoutNetworkFactoryPermission() throws Exception {
+        // Make sure NETWORK_FACTORY permission is not granted.
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
+        mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
+        final TestNetworkCallback cb = new TestNetworkCallback();
+        mCm.requestNetwork(new NetworkRequest.Builder()
+                        .clearCapabilities()
+                        .addTransportType(TRANSPORT_TEST)
+                        .addTransportType(TRANSPORT_CELLULAR)
+                        .build(),
+                cb);
+
+        final ArraySet<Integer> uids = new ArraySet<>();
+        uids.add(200);
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_TEST)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .setAllowedUids(uids)
+                .setOwnerUid(Process.myUid())
+                .setAdministratorUids(new int[] {Process.myUid()})
+                .build();
+        final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(TRANSPORT_TEST,
+                new LinkProperties(), nc);
+        agent.connect(true);
+        cb.expectAvailableThenValidatedCallbacks(agent);
+
+        uids.add(300);
+        uids.add(400);
+        nc.setAllowedUids(uids);
+        agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
+        if (mDeps.isAtLeastT()) {
+            // AllowedUids is not cleared even without the NETWORK_FACTORY permission
+            // because the caller is the owner of the network.
+            cb.expectCaps(agent, c -> c.getAllowedUids().equals(uids));
+        } else {
+            cb.assertNoCallback();
+        }
+    }
+
     @Test
     public void testAllowedUids() throws Exception {
         final int preferenceOrder =
@@ -19189,6 +19242,25 @@
         verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
     }
 
-    // Note : adding tests is ConnectivityServiceTest is deprecated, as it is too big for
+    private static final int EXPECTED_TEST_METHOD_COUNT = 332;
+
+    @Test
+    public void testTestMethodCount() {
+        final Class<?> testClass = this.getClass();
+
+        int actualTestMethodCount = 0;
+        for (final Method method : testClass.getDeclaredMethods()) {
+            if (method.isAnnotationPresent(Test.class)) {
+                actualTestMethodCount++;
+            }
+        }
+
+        assertEquals("Adding tests in ConnectivityServiceTest is deprecated, "
+                + "as it is too big for maintenance. Please consider adding new tests "
+                + "in subclasses of CSTest instead.",
+                EXPECTED_TEST_METHOD_COUNT, actualTestMethodCount);
+    }
+
+    // Note : adding tests in ConnectivityServiceTest is deprecated, as it is too big for
     // maintenance. Please consider adding new tests in subclasses of CSTest instead.
 }
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 881de56..979e0a1 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -41,6 +41,7 @@
 import static com.android.server.NsdService.DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF;
 import static com.android.server.NsdService.MdnsListener;
 import static com.android.server.NsdService.NO_TRANSACTION;
+import static com.android.server.NsdService.checkHostname;
 import static com.android.server.NsdService.parseTypeAndSubtype;
 import static com.android.testutils.ContextUtils.mockService;
 
@@ -53,6 +54,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -521,6 +523,56 @@
     }
 
     @Test
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    public void testDiscoverOnTetheringDownstream_DiscoveryManager() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final DiscoveryListener discListener = mock(DiscoveryListener.class);
+        client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
+        waitForIdle();
+
+        final ArgumentCaptor<MdnsServiceBrowserListener> discoverListenerCaptor =
+                ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
+        final InOrder discManagerOrder = inOrder(mDiscoveryManager);
+        final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
+        discManagerOrder.verify(mDiscoveryManager).registerListener(eq(serviceTypeWithLocalDomain),
+                discoverListenerCaptor.capture(), any());
+
+        final int interfaceIdx = 123;
+        final MdnsServiceInfo mockServiceInfo = new MdnsServiceInfo(
+                SERVICE_NAME, /* serviceInstanceName */
+                serviceTypeWithLocalDomain.split("\\."), /* serviceType */
+                List.of(), /* subtypes */
+                new String[] {"android", "local"}, /* hostName */
+                12345, /* port */
+                List.of(IPV4_ADDRESS),
+                List.of(IPV6_ADDRESS),
+                List.of(), /* textStrings */
+                List.of(), /* textEntries */
+                interfaceIdx, /* interfaceIndex */
+                null /* network */,
+                Instant.MAX /* expirationTime */);
+
+        // Verify service is found with the interface index
+        discoverListenerCaptor.getValue().onServiceNameDiscovered(
+                mockServiceInfo, false /* isServiceFromCache */);
+        final ArgumentCaptor<NsdServiceInfo> foundInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(foundInfoCaptor.capture());
+        final NsdServiceInfo foundInfo = foundInfoCaptor.getValue();
+        assertNull(foundInfo.getNetwork());
+        assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
+
+        // Using the returned service info to resolve or register callback uses the interface index
+        client.resolveService(foundInfo, mock(ResolveListener.class));
+        client.registerServiceInfoCallback(foundInfo, Runnable::run,
+                mock(ServiceInfoCallback.class));
+        waitForIdle();
+
+        discManagerOrder.verify(mDiscoveryManager, times(2)).registerListener(any(), any(), argThat(
+                o -> o.getNetwork() == null && o.getInterfaceIndex() == interfaceIdx));
+    }
+
+    @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
     @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testDiscoverOnBlackholeNetwork() throws Exception {
@@ -854,7 +906,7 @@
                 request.getServiceName().equals(ns.getServiceName())
                         && request.getServiceType().equals(ns.getServiceType())));
         verify(mMetrics).reportServiceResolutionStop(
-                true /* isLegacy */, resolveId, 10L /* durationMs */);
+                true /* isLegacy */, resolveId, 10L /* durationMs */, 0 /* sentQueryCount */);
     }
 
     @Test
@@ -928,7 +980,7 @@
                 request.getServiceName().equals(ns.getServiceName())
                         && request.getServiceType().equals(ns.getServiceType())));
         verify(mMetrics).reportServiceResolutionStop(
-                true /* isLegacy */, getAddrId, 10L /* durationMs */);
+                true /* isLegacy */, getAddrId, 10L /* durationMs */,  0 /* sentQueryCount */);
     }
 
     private void verifyUpdatedServiceInfo(NsdServiceInfo info, String serviceName,
@@ -1144,7 +1196,7 @@
                 Instant.MAX /* expirationTime */);
 
         // Verify onServiceNameDiscovered callback
-        listener.onServiceNameDiscovered(foundInfo, false /* isServiceFromCache */);
+        listener.onServiceNameDiscovered(foundInfo, true /* isServiceFromCache */);
         verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(argThat(info ->
                 info.getServiceName().equals(SERVICE_NAME)
                         // Service type in discovery callbacks has a dot at the end
@@ -1180,7 +1232,7 @@
         verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).requestStopWhenInactive();
         verify(mMetrics).reportServiceDiscoveryStop(false /* isLegacy */, discId,
                 10L /* durationMs */, 1 /* foundCallbackCount */, 1 /* lostCallbackCount */,
-                1 /* servicesCount */, 3 /* sentQueryCount */);
+                1 /* servicesCount */, 3 /* sentQueryCount */, true /* isServiceFromCache */);
     }
 
     @Test
@@ -1629,20 +1681,23 @@
         // Subtypes are not used for resolution, only for discovery
         assertEquals(Collections.emptyList(), optionsCaptor.getValue().getSubtypes());
 
+        final MdnsListener listener = listenerCaptor.getValue();
+        // Callbacks for query sent.
+        listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
+
         doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
         client.stopServiceResolution(resolveListener);
         waitForIdle();
 
         // Verify the listener has been unregistered.
-        final MdnsListener listener = listenerCaptor.getValue();
         verify(mDiscoveryManager, timeout(TIMEOUT_MS))
                 .unregisterListener(eq(constructedServiceType), eq(listener));
         verify(resolveListener, timeout(TIMEOUT_MS)).onResolutionStopped(argThat(ns ->
                 request.getServiceName().equals(ns.getServiceName())
                         && request.getServiceType().equals(ns.getServiceType())));
         verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).requestStopWhenInactive();
-        verify(mMetrics).reportServiceResolutionStop(
-                false /* isLegacy */, listener.mTransactionId, 10L /* durationMs */);
+        verify(mMetrics).reportServiceResolutionStop(false /* isLegacy */, listener.mTransactionId,
+                10L /* durationMs */, 1 /* sentQueryCount */);
     }
 
     @Test
@@ -1673,6 +1728,36 @@
     }
 
     @Test
+    public void TestCheckHostname() {
+        // Valid cases
+        assertTrue(checkHostname(null));
+        assertTrue(checkHostname("a"));
+        assertTrue(checkHostname("1"));
+        assertTrue(checkHostname("a-1234-bbbb-cccc000"));
+        assertTrue(checkHostname("A-1234-BBbb-CCCC000"));
+        assertTrue(checkHostname("1234-bbbb-cccc000"));
+        assertTrue(checkHostname("0123456789abcdef"
+                                + "0123456789abcdef"
+                                + "0123456789abcdef"
+                                + "0123456789abcde" // 63 characters
+                        ));
+
+        // Invalid cases
+        assertFalse(checkHostname("?"));
+        assertFalse(checkHostname("/"));
+        assertFalse(checkHostname("a-"));
+        assertFalse(checkHostname("B-"));
+        assertFalse(checkHostname("-A"));
+        assertFalse(checkHostname("-b"));
+        assertFalse(checkHostname("-1-"));
+        assertFalse(checkHostname("0123456789abcdef"
+                                + "0123456789abcdef"
+                                + "0123456789abcdef"
+                                + "0123456789abcdef" // 64 characters
+                        ));
+    }
+
+    @Test
     @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
     public void testEnablePlatformMdnsBackend() {
         final NsdManager client = connectClient(mService);
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 7bd2b56..ab81abc 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -21,6 +21,7 @@
 import static android.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED;
 
 import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
+import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -45,6 +46,7 @@
 import android.net.NetworkCapabilities;
 import android.net.TelephonyNetworkSpecifier;
 import android.os.Build;
+import android.os.Handler;
 import android.os.HandlerThread;
 import android.telephony.TelephonyManager;
 
@@ -56,6 +58,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
 
 import org.junit.After;
 import org.junit.Rule;
@@ -85,6 +88,7 @@
 
     private static final int SUBSCRIPTION_COUNT = 2;
     private static final int TEST_SUBSCRIPTION_ID = 1;
+    private static final int TIMEOUT_MS = 1_000;
 
     @NonNull private final Context mContext;
     @NonNull private final TelephonyManager mTelephonyManager;
@@ -97,13 +101,16 @@
     private final String mTestPkg = "com.android.server.connectivity.test";
     private final BroadcastReceiver mMultiSimBroadcastReceiver;
     @NonNull private final HandlerThread mHandlerThread;
+    @NonNull private final Handler mCsHandler;
+    @NonNull private final HandlerThread mCsHandlerThread;
 
     public class TestCarrierPrivilegeAuthenticator extends CarrierPrivilegeAuthenticator {
         TestCarrierPrivilegeAuthenticator(@NonNull final Context c,
                 @NonNull final Dependencies deps,
-                @NonNull final TelephonyManager t) {
+                @NonNull final TelephonyManager t,
+                @NonNull final Handler handler) {
             super(c, deps, t, mTelephonyManagerShim, true /* requestRestrictedWifiEnabled */,
-                    mListener);
+                    mListener, handler);
         }
         @Override
         protected int getSubId(int slotIndex) {
@@ -112,8 +119,11 @@
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
         mHandlerThread.quit();
+        mHandlerThread.join();
+        mCsHandlerThread.quit();
+        mCsHandlerThread.join();
     }
 
     /** Parameters to test both using callbacks or the old broadcast */
@@ -141,8 +151,14 @@
         final ApplicationInfo applicationInfo = new ApplicationInfo();
         applicationInfo.uid = mCarrierConfigPkgUid;
         doReturn(applicationInfo).when(mPackageManager).getApplicationInfo(eq(mTestPkg), anyInt());
-        mCarrierPrivilegeAuthenticator =
-                new TestCarrierPrivilegeAuthenticator(mContext, deps, mTelephonyManager);
+        mCsHandlerThread = new HandlerThread(
+                CarrierPrivilegeAuthenticatorTest.class.getSimpleName() + "-CsHandlerThread");
+        mCsHandlerThread.start();
+        mCsHandler = new Handler(mCsHandlerThread.getLooper());
+        mCarrierPrivilegeAuthenticator = new TestCarrierPrivilegeAuthenticator(mContext, deps,
+                mTelephonyManager, mCsHandler);
+        mCarrierPrivilegeAuthenticator.start();
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
         verify(mContext).registerReceiver(receiverCaptor.capture(), argThat(filter ->
@@ -178,7 +194,9 @@
         assertNotNull(initialListeners.get(1));
         assertEquals(2, initialListeners.size());
 
-        initialListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            initialListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
 
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_CELLULAR)
@@ -201,10 +219,10 @@
 
         doReturn(1).when(mTelephonyManager).getActiveModemCount();
 
-        // This is a little bit cavalier in that the call to onReceive is not on the handler
-        // thread that was specified in registerReceiver.
-        // TODO : capture the handler and call this on it if this causes flakiness.
-        mMultiSimBroadcastReceiver.onReceive(mContext, buildTestMultiSimConfigBroadcastIntent());
+        visibleOnHandlerThread(mCsHandler, () -> {
+            mMultiSimBroadcastReceiver.onReceive(mContext,
+                    buildTestMultiSimConfigBroadcastIntent());
+        });
         // Check all listeners have been removed
         for (CarrierPrivilegesListenerShim listener : initialListeners.values()) {
             verify(mTelephonyManagerShim).removeCarrierPrivilegesListener(eq(listener));
@@ -216,7 +234,9 @@
         assertNotNull(newListeners.get(0));
         assertEquals(1, newListeners.size());
 
-        newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
 
         final TelephonyNetworkSpecifier specifier =
                 new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
@@ -235,12 +255,17 @@
     public void testCarrierPrivilegesLostDueToCarrierServiceUpdate() throws Exception {
         final CarrierPrivilegesListenerShim l = getCarrierPrivilegesListeners().get(0);
 
-        l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
-        l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+            l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
+        });
         if (mUseCallbacks) {
             verify(mListener).accept(eq(mCarrierConfigPkgUid), eq(TEST_SUBSCRIPTION_ID));
         }
-        l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+
+        visibleOnHandlerThread(mCsHandler, () -> {
+            l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+        });
         if (mUseCallbacks) {
             verify(mListener).accept(eq(mCarrierConfigPkgUid + 1), eq(TEST_SUBSCRIPTION_ID));
         }
@@ -260,8 +285,10 @@
         final ApplicationInfo applicationInfo = new ApplicationInfo();
         applicationInfo.uid = mCarrierConfigPkgUid + 1;
         doReturn(applicationInfo).when(mPackageManager).getApplicationInfo(eq(mTestPkg), anyInt());
-        listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[] {});
-        listener.onCarrierServiceChanged(null, applicationInfo.uid);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[]{});
+            listener.onCarrierServiceChanged(null, applicationInfo.uid);
+        });
 
         assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
                 mCarrierConfigPkgUid, nc));
@@ -272,7 +299,9 @@
     @Test
     public void testDefaultSubscription() throws Exception {
         final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
-        listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
 
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
         ncBuilder.addTransportType(TRANSPORT_CELLULAR);
@@ -297,7 +326,9 @@
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testNetworkCapabilitiesContainOneSubId() throws Exception {
         final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
-        listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
 
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
         ncBuilder.addTransportType(TRANSPORT_WIFI);
@@ -311,7 +342,9 @@
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testNetworkCapabilitiesContainTwoSubIds() throws Exception {
         final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
-        listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        visibleOnHandlerThread(mCsHandler, () -> {
+            listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+        });
 
         final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
         ncBuilder.addTransportType(TRANSPORT_WIFI);
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index 88044be..72dde7f 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -418,21 +419,6 @@
         inOrder.verify(mDeps).generateIpv6Address(eq(BASE_IFACE),
                 eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(NAT64_PREFIX_STRING), eq(MARK));
 
-        // Open, configure and bring up the tun interface.
-        inOrder.verify(mDeps).createTunInterface(eq(STACKED_IFACE));
-        inOrder.verify(mDeps).adoptFd(eq(TUN_FD));
-        inOrder.verify(mDeps).getInterfaceIndex(eq(STACKED_IFACE));
-        inOrder.verify(mNetd).interfaceSetEnableIPv6(eq(STACKED_IFACE), eq(false /* enable */));
-        inOrder.verify(mDeps).detectMtu(eq(NAT64_PREFIX_STRING), eq(GOOGLE_DNS_4), eq(MARK));
-        inOrder.verify(mNetd).interfaceSetMtu(eq(STACKED_IFACE),
-                eq(1472 /* ETHER_MTU(1500) - MTU_DELTA(28) */));
-        inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
-                STACKED_IFACE.equals(cfg.ifName)
-                && XLAT_LOCAL_IPV4ADDR_STRING.equals(cfg.ipv4Addr)
-                && (32 == cfg.prefixLength)
-                && "".equals(cfg.hwAddr)
-                && assertContainsFlag(cfg.flags, IF_STATE_UP)));
-
         // Open and configure 464xlat read/write sockets.
         inOrder.verify(mDeps).openPacketSocket();
         inOrder.verify(mDeps).adoptFd(eq(PACKET_SOCK_FD));
@@ -449,6 +435,21 @@
                 argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
                 eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
 
+        // Open, configure and bring up the tun interface.
+        inOrder.verify(mDeps).createTunInterface(eq(STACKED_IFACE));
+        inOrder.verify(mDeps).adoptFd(eq(TUN_FD));
+        inOrder.verify(mDeps).getInterfaceIndex(eq(STACKED_IFACE));
+        inOrder.verify(mNetd).interfaceSetEnableIPv6(eq(STACKED_IFACE), eq(false /* enable */));
+        inOrder.verify(mDeps).detectMtu(eq(NAT64_PREFIX_STRING), eq(GOOGLE_DNS_4), eq(MARK));
+        inOrder.verify(mNetd).interfaceSetMtu(eq(STACKED_IFACE),
+                eq(1472 /* ETHER_MTU(1500) - MTU_DELTA(28) */));
+        inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+                STACKED_IFACE.equals(cfg.ifName)
+                        && XLAT_LOCAL_IPV4ADDR_STRING.equals(cfg.ipv4Addr)
+                        && (32 == cfg.prefixLength)
+                        && "".equals(cfg.hwAddr)
+                        && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+
         // Start clatd.
         inOrder.verify(mDeps).startClatd(
                 argThat(fd -> Objects.equals(TUN_PFD.getFileDescriptor(), fd)),
@@ -526,13 +527,13 @@
                     + "v4: /192.0.0.46, v6: /2001:db8:0:b11::464, pfx96: /64:ff9b::, "
                     + "pid: 10483, cookie: 27149", dumpStrings[0].trim());
             assertEquals("Forwarding rules:", dumpStrings[1].trim());
-            assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif",
+            assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif (packets bytes)",
                     dumpStrings[2].trim());
-            assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
+            assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001 (0 0)",
                     dumpStrings[3].trim());
-            assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
+            assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif (packets bytes)",
                     dumpStrings[4].trim());
-            assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
+            assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether (0 0)",
                     dumpStrings[5].trim());
         } else {
             assertEquals(1, dumpStrings.length);
@@ -630,9 +631,13 @@
             public int createTunInterface(@NonNull String tuniface) throws IOException {
                 throw new IOException();
             }
+            @Override
+            public IBpfMap<CookieTagMapKey, CookieTagMapValue> getBpfCookieTagMap() {
+                return  mock(IBpfMap.class);
+            }
         }
         checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
-                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -643,9 +648,14 @@
                     throws IOException {
                 throw new IOException();
             }
+
+            @Override
+            public IBpfMap<CookieTagMapKey, CookieTagMapValue> getBpfCookieTagMap() {
+                return  mock(IBpfMap.class);
+            }
         }
         checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
-                false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+                true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
     @Test
@@ -656,7 +666,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
@@ -668,7 +678,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
     }
 
@@ -681,7 +691,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
@@ -694,7 +704,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
@@ -721,7 +731,7 @@
                 throw new IOException();
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 
@@ -733,7 +743,7 @@
                 return null;
             }
         }
-        checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+        checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
                 true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index 44512bb..ea3d2dd 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -29,8 +29,6 @@
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
 import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
 
-import static com.android.testutils.MiscAsserts.assertContainsExactly;
-import static com.android.testutils.MiscAsserts.assertContainsStringsExactly;
 import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
 
 import static org.junit.Assert.assertEquals;
@@ -38,12 +36,12 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.net.ConnectivitySettingsManager;
@@ -74,7 +72,6 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -122,29 +119,6 @@
         assertFieldCountEquals(3, ResolverOptionsParcel.class);
     }
 
-    private void assertResolverParamsEquals(@NonNull ResolverParamsParcel actual,
-            @NonNull ResolverParamsParcel expected) {
-        assertEquals(actual.netId, expected.netId);
-        assertEquals(actual.sampleValiditySeconds, expected.sampleValiditySeconds);
-        assertEquals(actual.successThreshold, expected.successThreshold);
-        assertEquals(actual.minSamples, expected.minSamples);
-        assertEquals(actual.maxSamples, expected.maxSamples);
-        assertEquals(actual.baseTimeoutMsec, expected.baseTimeoutMsec);
-        assertEquals(actual.retryCount, expected.retryCount);
-        assertContainsStringsExactly(actual.servers, expected.servers);
-        assertContainsStringsExactly(actual.domains, expected.domains);
-        assertEquals(actual.tlsName, expected.tlsName);
-        assertContainsStringsExactly(actual.tlsServers, expected.tlsServers);
-        assertContainsStringsExactly(actual.tlsFingerprints, expected.tlsFingerprints);
-        assertEquals(actual.caCertificate, expected.caCertificate);
-        assertEquals(actual.tlsConnectTimeoutMs, expected.tlsConnectTimeoutMs);
-        assertResolverOptionsEquals(actual.resolverOptions, expected.resolverOptions);
-        assertContainsExactly(actual.transportTypes, expected.transportTypes);
-        assertEquals(actual.meteredNetwork, expected.meteredNetwork);
-        assertEquals(actual.dohParams, expected.dohParams);
-        assertFieldCountEquals(18, ResolverParamsParcel.class);
-    }
-
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -365,11 +339,6 @@
         mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
         mDnsManager.flushVmDnsCache();
 
-        final ArgumentCaptor<ResolverParamsParcel> resolverParamsParcelCaptor =
-                ArgumentCaptor.forClass(ResolverParamsParcel.class);
-        verify(mMockDnsResolver, times(1)).setResolverConfiguration(
-                resolverParamsParcelCaptor.capture());
-        final ResolverParamsParcel actualParams = resolverParamsParcelCaptor.getValue();
         final ResolverParamsParcel expectedParams = new ResolverParamsParcel();
         expectedParams.netId = TEST_NETID;
         expectedParams.sampleValiditySeconds = TEST_DEFAULT_SAMPLE_VALIDITY_SECONDS;
@@ -384,7 +353,8 @@
         expectedParams.resolverOptions = null;
         expectedParams.meteredNetwork = true;
         expectedParams.dohParams = null;
-        assertResolverParamsEquals(actualParams, expectedParams);
+        expectedParams.interfaceNames = new String[]{TEST_IFACENAME};
+        verify(mMockDnsResolver, times(1)).setResolverConfiguration(eq(expectedParams));
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
index 6c2c256..5c994f5 100644
--- a/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/MulticastRoutingCoordinatorServiceTest.kt
@@ -402,15 +402,18 @@
                 mService.getVirtualInterfaceIndex(mIfName1), oifsUpdate)
         val mf6cctlDel = createStructMf6cctl(mSourceAddress, mGroupAddressScope5,
                 mService.getVirtualInterfaceIndex(mIfName1), mEmptyOifs)
+        val ifName1Copy = String(mIfName1.toCharArray())
+        val ifName2Copy = String(mIfName2.toCharArray())
+        val ifName3Copy = String(mIfName3.toCharArray())
 
         verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlAdd))
 
-        applyMulticastForwardNone(mIfName1, mIfName2)
+        applyMulticastForwardNone(ifName1Copy, ifName2Copy)
         mLooper.dispatchAll()
 
         verify(mDeps).setsockoptMrt6AddMfc(eq(mFd), eq(mf6cctlUpdate))
 
-        applyMulticastForwardNone(mIfName1, mIfName3)
+        applyMulticastForwardNone(ifName1Copy, ifName3Copy)
         mLooper.dispatchAll()
 
         verify(mDeps, timeout(TIMEOUT_MS).times(1)).setsockoptMrt6DelMfc(eq(mFd), eq(mf6cctlDel))
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index 7121ed4..727db58 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -113,6 +113,7 @@
     private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
     private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
     private static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
+    private static final NetworkCapabilities BT_CAPABILITIES = new NetworkCapabilities();
     static {
         CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
         CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
@@ -128,6 +129,9 @@
         VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_VPN);
         VPN_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
         VPN_CAPABILITIES.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+
+        BT_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_BLUETOOTH);
+        BT_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
     }
 
     /**
@@ -159,7 +163,9 @@
     @Mock NetworkAgentInfo mWifiNai;
     @Mock NetworkAgentInfo mCellNai;
     @Mock NetworkAgentInfo mVpnNai;
+    @Mock NetworkAgentInfo mBluetoothNai;
     @Mock NetworkInfo mNetworkInfo;
+    @Mock NetworkInfo mEmptyNetworkInfo;
     ArgumentCaptor<Notification> mCaptor;
 
     NetworkNotificationManager mManager;
@@ -174,6 +180,8 @@
         mCellNai.networkInfo = mNetworkInfo;
         mVpnNai.networkCapabilities = VPN_CAPABILITIES;
         mVpnNai.networkInfo = mNetworkInfo;
+        mBluetoothNai.networkCapabilities = BT_CAPABILITIES;
+        mBluetoothNai.networkInfo = mEmptyNetworkInfo;
         mDisplayMetrics.density = 2.275f;
         doReturn(true).when(mVpnNai).isVPN();
         doReturn(mResources).when(mCtx).getResources();
@@ -542,10 +550,11 @@
                 R.string.wifi_no_internet_detailed);
     }
 
-    private void runTelephonySignInNotificationTest(String testTitle, String testContents) {
+    private void runSignInNotificationTest(NetworkAgentInfo nai, String testTitle,
+            String testContents) {
         final int id = 101;
         final String tag = NetworkNotificationManager.tagFor(id);
-        mManager.showNotification(id, SIGN_IN, mCellNai, null, null, false);
+        mManager.showNotification(id, SIGN_IN, nai, null, null, false);
 
         final ArgumentCaptor<Notification> noteCaptor = ArgumentCaptor.forClass(Notification.class);
         verify(mNotificationManager).notify(eq(tag), eq(SIGN_IN.eventId), noteCaptor.capture());
@@ -565,7 +574,7 @@
         doReturn(testContents).when(mResources).getString(
                 R.string.mobile_network_available_no_internet_detailed, TEST_OPERATOR_NAME);
 
-        runTelephonySignInNotificationTest(testTitle, testContents);
+        runSignInNotificationTest(mCellNai, testTitle, testContents);
     }
 
     @Test
@@ -579,6 +588,21 @@
         doReturn(testContents).when(mResources).getString(
                 R.string.mobile_network_available_no_internet_detailed_unknown_carrier);
 
-        runTelephonySignInNotificationTest(testTitle, testContents);
+        runSignInNotificationTest(mCellNai, testTitle, testContents);
+    }
+
+    @Test
+    public void testBluetoothSignInNotification_EmptyNotificationContents() {
+        final String testTitle = "Test title";
+        final String testContents = "Test contents";
+        doReturn(testTitle).when(mResources).getString(
+                R.string.network_available_sign_in, 0);
+        doReturn(testContents).when(mResources).getString(
+                eq(R.string.network_available_sign_in_detailed), any());
+
+        runSignInNotificationTest(mBluetoothNai, testTitle, testContents);
+        // The details should be queried with an empty string argument. In practice the notification
+        // contents may just be an empty string, since the default translation just outputs the arg.
+        verify(mResources).getString(eq(R.string.network_available_sign_in_detailed), eq(""));
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
index 193078b..7885325 100644
--- a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
@@ -18,17 +18,22 @@
 import android.Manifest
 import android.app.role.OnRoleHoldersChangedListener
 import android.app.role.RoleManager
+import android.content.BroadcastReceiver
 import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
-import android.content.pm.UserInfo
 import android.os.Build
 import android.os.Handler
+import android.os.Looper
 import android.os.UserHandle
+import android.os.UserManager
 import android.util.ArraySet
-import com.android.server.makeMockUserManager
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import java.util.concurrent.Executor
+import java.util.function.Consumer
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,30 +41,32 @@
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import java.util.concurrent.Executor
-import java.util.function.Consumer
 
-private const val USER = 0
-val USER_INFO = UserInfo(USER, "" /* name */, UserInfo.FLAG_PRIMARY)
-val USER_HANDLE = UserHandle(USER)
 private const val PRIMARY_USER = 0
 private const val SECONDARY_USER = 10
 private val PRIMARY_USER_HANDLE = UserHandle.of(PRIMARY_USER)
 private val SECONDARY_USER_HANDLE = UserHandle.of(SECONDARY_USER)
+
 // sms app names
 private const val SMS_APP1 = "sms_app_1"
 private const val SMS_APP2 = "sms_app_2"
+
 // sms app ids
 private const val SMS_APP_ID1 = 100
 private const val SMS_APP_ID2 = 101
+
 // UID for app1 and app2 on primary user
 // These app could become default sms app for user1
 private val PRIMARY_USER_SMS_APP_UID1 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID1)
 private val PRIMARY_USER_SMS_APP_UID2 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID2)
+
 // UID for app1 and app2 on secondary user
 // These app could become default sms app for user2
 private val SECONDARY_USER_SMS_APP_UID1 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID1)
@@ -69,154 +76,259 @@
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
 class SatelliteAccessControllerTest {
     private val context = mock(Context::class.java)
-    private val mPackageManager = mock(PackageManager::class.java)
-    private val mHandler = mock(Handler::class.java)
-    private val mRoleManager =
-        mock(SatelliteAccessController.Dependencies::class.java)
+    private val primaryUserContext = mock(Context::class.java)
+    private val secondaryUserContext = mock(Context::class.java)
+    private val mPackageManagerPrimaryUser = mock(PackageManager::class.java)
+    private val mPackageManagerSecondaryUser = mock(PackageManager::class.java)
+    private val mDeps = mock(SatelliteAccessController.Dependencies::class.java)
     private val mCallback = mock(Consumer::class.java) as Consumer<Set<Int>>
-    private val mSatelliteAccessController =
-        SatelliteAccessController(context, mRoleManager, mCallback, mHandler)
+    private val userManager = mock(UserManager::class.java)
+    private val mHandler = Handler(Looper.getMainLooper())
+    private var mSatelliteAccessController =
+        SatelliteAccessController(context, mDeps, mCallback, mHandler)
     private lateinit var mRoleHolderChangedListener: OnRoleHoldersChangedListener
+    private lateinit var mUserRemovedReceiver: BroadcastReceiver
+
+    private fun <T> mockService(name: String, clazz: Class<T>, service: T) {
+        doReturn(name).`when`(context).getSystemServiceName(clazz)
+        doReturn(service).`when`(context).getSystemService(name)
+        if (context.getSystemService(clazz) == null) {
+            // Test is using mockito-extended
+            doReturn(service).`when`(context).getSystemService(clazz)
+        }
+    }
+
     @Before
     @Throws(PackageManager.NameNotFoundException::class)
     fun setup() {
-        makeMockUserManager(USER_INFO, USER_HANDLE)
-        doReturn(context).`when`(context).createContextAsUser(any(), anyInt())
-        doReturn(mPackageManager).`when`(context).packageManager
+        doReturn(emptyList<UserHandle>()).`when`(userManager).getUserHandles(true)
+        mockService(Context.USER_SERVICE, UserManager::class.java, userManager)
 
-        doReturn(PackageManager.PERMISSION_GRANTED)
-            .`when`(mPackageManager)
-            .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
-        doReturn(PackageManager.PERMISSION_GRANTED)
-            .`when`(mPackageManager)
-            .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP2)
+        doReturn(primaryUserContext).`when`(context).createContextAsUser(PRIMARY_USER_HANDLE, 0)
+        doReturn(mPackageManagerPrimaryUser).`when`(primaryUserContext).packageManager
 
-        // Initialise default message application primary user package1
+        doReturn(secondaryUserContext).`when`(context).createContextAsUser(SECONDARY_USER_HANDLE, 0)
+        doReturn(mPackageManagerSecondaryUser).`when`(secondaryUserContext).packageManager
+
+        for (app in listOf(SMS_APP1, SMS_APP2)) {
+            doReturn(PackageManager.PERMISSION_GRANTED)
+                .`when`(mPackageManagerPrimaryUser)
+                .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, app)
+            doReturn(PackageManager.PERMISSION_GRANTED)
+                .`when`(mPackageManagerSecondaryUser)
+                .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, app)
+        }
+
+        // Initialise message application primary user package1
         val applicationInfo1 = ApplicationInfo()
         applicationInfo1.uid = PRIMARY_USER_SMS_APP_UID1
         doReturn(applicationInfo1)
-            .`when`(mPackageManager)
+            .`when`(mPackageManagerPrimaryUser)
             .getApplicationInfo(eq(SMS_APP1), anyInt())
 
-        // Initialise default message application primary user package2
+        // Initialise message application primary user package2
         val applicationInfo2 = ApplicationInfo()
         applicationInfo2.uid = PRIMARY_USER_SMS_APP_UID2
         doReturn(applicationInfo2)
-            .`when`(mPackageManager)
+            .`when`(mPackageManagerPrimaryUser)
             .getApplicationInfo(eq(SMS_APP2), anyInt())
 
-        // Get registered listener using captor
-        val listenerCaptor = ArgumentCaptor.forClass(
-            OnRoleHoldersChangedListener::class.java
-        )
-        mSatelliteAccessController.start()
-        verify(mRoleManager).addOnRoleHoldersChangedListenerAsUser(
-            any(Executor::class.java), listenerCaptor.capture(), any(UserHandle::class.java))
-        mRoleHolderChangedListener = listenerCaptor.value
+        // Initialise message application secondary user package1
+        val applicationInfo3 = ApplicationInfo()
+        applicationInfo3.uid = SECONDARY_USER_SMS_APP_UID1
+        doReturn(applicationInfo3)
+            .`when`(mPackageManagerSecondaryUser)
+            .getApplicationInfo(eq(SMS_APP1), anyInt())
+
+        // Initialise message application secondary user package2
+        val applicationInfo4 = ApplicationInfo()
+        applicationInfo4.uid = SECONDARY_USER_SMS_APP_UID2
+        doReturn(applicationInfo4)
+            .`when`(mPackageManagerSecondaryUser)
+            .getApplicationInfo(eq(SMS_APP2), anyInt())
     }
 
     @Test
     fun test_onRoleHoldersChanged_SatelliteFallbackUid_Changed_SingleUser() {
-        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            PRIMARY_USER_HANDLE)
+        startSatelliteAccessController()
+        doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback, never()).accept(any())
 
         // check DEFAULT_MESSAGING_APP1 is available as satellite network fallback uid
         doReturn(listOf(SMS_APP1))
-            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+            .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
 
         // check SMS_APP2 is available as satellite network Fallback uid
-        doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            PRIMARY_USER_HANDLE)
+        doReturn(listOf(SMS_APP2)).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
 
         // check no uid is available as satellite network fallback uid
-        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            PRIMARY_USER_HANDLE)
+        doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback).accept(ArraySet())
     }
 
     @Test
     fun test_onRoleHoldersChanged_NoSatelliteCommunicationPermission() {
-        doReturn(listOf<Any>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            PRIMARY_USER_HANDLE)
+        startSatelliteAccessController()
+        doReturn(listOf<Any>()).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback, never()).accept(any())
 
         // check DEFAULT_MESSAGING_APP1 is not available as satellite network fallback uid
         // since satellite communication permission not available.
         doReturn(PackageManager.PERMISSION_DENIED)
-            .`when`(mPackageManager)
+            .`when`(mPackageManagerPrimaryUser)
             .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
         doReturn(listOf(SMS_APP1))
-            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+            .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback, never()).accept(any())
     }
 
     @Test
     fun test_onRoleHoldersChanged_RoleSms_NotAvailable() {
+        startSatelliteAccessController()
         doReturn(listOf(SMS_APP1))
-            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
-        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_BROWSER,
-            PRIMARY_USER_HANDLE)
+            .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        mRoleHolderChangedListener.onRoleHoldersChanged(
+            RoleManager.ROLE_BROWSER,
+            PRIMARY_USER_HANDLE
+        )
         verify(mCallback, never()).accept(any())
     }
 
     @Test
     fun test_onRoleHoldersChanged_SatelliteNetworkFallbackUid_Changed_multiUser() {
-        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            PRIMARY_USER_HANDLE)
+        startSatelliteAccessController()
+        doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback, never()).accept(any())
 
         // check SMS_APP1 is available as satellite network fallback uid at primary user
         doReturn(listOf(SMS_APP1))
-            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+            .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
 
         // check SMS_APP2 is available as satellite network fallback uid at primary user
-        doReturn(listOf(SMS_APP2)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            PRIMARY_USER_HANDLE)
+        doReturn(listOf(SMS_APP2)).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
         verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
 
         // check SMS_APP1 is available as satellite network fallback uid at secondary user
-        val applicationInfo1 = ApplicationInfo()
-        applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID1
-        doReturn(applicationInfo1).`when`(mPackageManager)
-            .getApplicationInfo(eq(SMS_APP1), anyInt())
-        doReturn(listOf(SMS_APP1)).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            SECONDARY_USER_HANDLE)
+        doReturn(listOf(SMS_APP1)).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            SECONDARY_USER_HANDLE
+        )
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
         verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2, SECONDARY_USER_SMS_APP_UID1))
 
         // check no uid is available as satellite network fallback uid at primary user
-        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            PRIMARY_USER_HANDLE)
-        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS,
-            PRIMARY_USER_HANDLE)
+        doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
+        mRoleHolderChangedListener.onRoleHoldersChanged(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
         verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID1))
 
         // check SMS_APP2 is available as satellite network fallback uid at secondary user
-        applicationInfo1.uid = SECONDARY_USER_SMS_APP_UID2
-        doReturn(applicationInfo1).`when`(mPackageManager)
-            .getApplicationInfo(eq(SMS_APP2), anyInt())
         doReturn(listOf(SMS_APP2))
-            .`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+            .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
         verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID2))
 
         // check no uid is available as satellite network fallback uid at secondary user
-        doReturn(listOf<String>()).`when`(mRoleManager).getRoleHoldersAsUser(RoleManager.ROLE_SMS,
-            SECONDARY_USER_HANDLE)
+        doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            SECONDARY_USER_HANDLE
+        )
         mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
         verify(mCallback).accept(ArraySet())
     }
+
+    @Test
+    fun test_SatelliteFallbackUidCallback_OnUserRemoval() {
+        startSatelliteAccessController()
+        // check SMS_APP2 is available as satellite network fallback uid at primary user
+        doReturn(listOf(SMS_APP2)).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            PRIMARY_USER_HANDLE
+        )
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+        // check SMS_APP1 is available as satellite network fallback uid at secondary user
+        doReturn(listOf(SMS_APP1)).`when`(mDeps).getRoleHoldersAsUser(
+            RoleManager.ROLE_SMS,
+            SECONDARY_USER_HANDLE
+        )
+        mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+        verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2, SECONDARY_USER_SMS_APP_UID1))
+
+        val userRemovalIntent = Intent(Intent.ACTION_USER_REMOVED)
+        userRemovalIntent.putExtra(Intent.EXTRA_USER, SECONDARY_USER_HANDLE)
+        mUserRemovedReceiver.onReceive(context, userRemovalIntent)
+        verify(mCallback, times(2)).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+    }
+
+    @Test
+    fun testOnStartUpCallbackSatelliteFallbackUidWithExistingUsers() {
+        doReturn(
+            listOf(PRIMARY_USER_HANDLE)
+        ).`when`(userManager).getUserHandles(true)
+        doReturn(listOf(SMS_APP1))
+            .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+        // At start up, SatelliteAccessController must call CS callback with existing users'
+        // default messaging apps uids.
+        startSatelliteAccessController()
+        verify(mCallback, timeout(500)).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+    }
+
+    private fun startSatelliteAccessController() {
+        mSatelliteAccessController.start()
+        // Get registered listener using captor
+        val listenerCaptor = ArgumentCaptor.forClass(OnRoleHoldersChangedListener::class.java)
+        verify(mDeps).addOnRoleHoldersChangedListenerAsUser(
+            any(Executor::class.java),
+            listenerCaptor.capture(),
+            any(UserHandle::class.java)
+        )
+        mRoleHolderChangedListener = listenerCaptor.value
+
+        // Get registered receiver using captor
+        val userRemovedReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver::class.java)
+        verify(context).registerReceiver(
+            userRemovedReceiverCaptor.capture(),
+            any(IntentFilter::class.java),
+            isNull(),
+            any(Handler::class.java)
+        )
+         mUserRemovedReceiver = userRemovedReceiverCaptor.value
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index b8ebf0f..df48f6c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -286,7 +286,6 @@
 
         postSync { socketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
         verify(mockInterfaceAdvertiser1).destroyNow()
-        postSync { intAdvCbCaptor.value.onDestroyed(mockSocket1) }
         verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE2))
     }
 
@@ -364,10 +363,10 @@
         verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
         verify(cb).onOffloadStop(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
 
-        // Interface advertisers call onDestroyed after sending exit announcements
-        postSync { intAdvCbCaptor1.value.onDestroyed(mockSocket1) }
+        // Interface advertisers call onAllServicesRemoved after sending exit announcements
+        postSync { intAdvCbCaptor1.value.onAllServicesRemoved(mockSocket1) }
         verify(socketProvider, never()).unrequestSocket(any())
-        postSync { intAdvCbCaptor2.value.onDestroyed(mockSocket2) }
+        postSync { intAdvCbCaptor2.value.onAllServicesRemoved(mockSocket2) }
         verify(socketProvider).unrequestSocket(socketCb)
     }
 
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index 5251e2a..b5c0132 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -18,6 +18,8 @@
 
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
@@ -65,8 +67,9 @@
     private static final String SERVICE_TYPE_2 = "_test._tcp.local";
     private static final Network NETWORK_1 = Mockito.mock(Network.class);
     private static final Network NETWORK_2 = Mockito.mock(Network.class);
+    private static final int INTERFACE_INDEX_NULL_NETWORK = 123;
     private static final SocketKey SOCKET_KEY_NULL_NETWORK =
-            new SocketKey(null /* network */, 999 /* interfaceIndex */);
+            new SocketKey(null /* network */, INTERFACE_INDEX_NULL_NETWORK);
     private static final SocketKey SOCKET_KEY_NETWORK_1 =
             new SocketKey(NETWORK_1, 998 /* interfaceIndex */);
     private static final SocketKey SOCKET_KEY_NETWORK_2 =
@@ -97,6 +100,8 @@
     private HandlerThread thread;
     private Handler handler;
 
+    private int createdServiceTypeClientCount;
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -106,11 +111,13 @@
         handler = new Handler(thread.getLooper());
         doReturn(thread.getLooper()).when(socketClient).getLooper();
         doReturn(true).when(socketClient).supportsRequestingSpecificNetworks();
+        createdServiceTypeClientCount = 0;
         discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient,
                 sharedLog, MdnsFeatureFlags.newBuilder().build()) {
                     @Override
                     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
                             @NonNull SocketKey socketKey) {
+                        createdServiceTypeClientCount++;
                         final Pair<String, SocketKey> perSocketServiceType =
                                 Pair.create(serviceType, socketKey);
                         if (perSocketServiceType.equals(PER_SOCKET_SERVICE_TYPE_1_NULL_NETWORK)) {
@@ -128,6 +135,7 @@
                                 PER_SOCKET_SERVICE_TYPE_2_NETWORK_2)) {
                             return mockServiceTypeClientType2Network2;
                         }
+                        fail("Unexpected perSocketServiceType: " + perSocketServiceType);
                         return null;
                     }
                 };
@@ -324,7 +332,6 @@
 
         // Receive a response, it should be processed on the client.
         final MdnsPacket response = createMdnsPacket(SERVICE_TYPE_1);
-        final int ifIndex = 1;
         runOnHandler(() -> discoveryManager.onResponseReceived(response, SOCKET_KEY_NULL_NETWORK));
         verify(mockServiceTypeClientType1NullNetwork).processResponse(
                 response, SOCKET_KEY_NULL_NETWORK);
@@ -350,6 +357,39 @@
         verify(socketClient, never()).stopDiscovery();
     }
 
+    @Test
+    public void testInterfaceIndexRequested_OnlyUsesSelectedInterface() throws IOException {
+        final MdnsSearchOptions searchOptions =
+                MdnsSearchOptions.newBuilder()
+                        .setNetwork(null /* network */)
+                        .setInterfaceIndex(INTERFACE_INDEX_NULL_NETWORK)
+                        .build();
+
+        final SocketCreationCallback callback = expectSocketCreationCallback(
+                SERVICE_TYPE_1, mockListenerOne, searchOptions);
+        final SocketKey unusedIfaceKey = new SocketKey(null, INTERFACE_INDEX_NULL_NETWORK + 1);
+        final SocketKey matchingIfaceWithNetworkKey =
+                new SocketKey(Mockito.mock(Network.class), INTERFACE_INDEX_NULL_NETWORK);
+        runOnHandler(() -> {
+            callback.onSocketCreated(unusedIfaceKey);
+            callback.onSocketCreated(matchingIfaceWithNetworkKey);
+            callback.onSocketCreated(SOCKET_KEY_NULL_NETWORK);
+            callback.onSocketCreated(SOCKET_KEY_NETWORK_1);
+        });
+        // Only the client for INTERFACE_INDEX_NULL_NETWORK is created
+        verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(
+                mockListenerOne, searchOptions);
+        assertEquals(1, createdServiceTypeClientCount);
+
+        runOnHandler(() -> {
+            callback.onSocketDestroyed(SOCKET_KEY_NETWORK_1);
+            callback.onSocketDestroyed(SOCKET_KEY_NULL_NETWORK);
+            callback.onSocketDestroyed(matchingIfaceWithNetworkKey);
+            callback.onSocketDestroyed(unusedIfaceKey);
+        });
+        verify(mockServiceTypeClientType1NullNetwork).notifySocketDestroyed();
+    }
+
     private MdnsPacket createMdnsPacket(String serviceType) {
         final String[] type = TextUtils.split(serviceType, "\\.");
         final ArrayList<String> name = new ArrayList<>(type.length + 1);
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 28608bb..629ac67 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -18,7 +18,6 @@
 
 import android.net.InetAddresses.parseNumericAddress
 import android.net.LinkAddress
-import android.net.nsd.NsdManager
 import android.net.nsd.NsdServiceInfo
 import android.os.Build
 import android.os.HandlerThread
@@ -55,6 +54,7 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.inOrder
 
 private const val LOG_TAG = "testlogtag"
 private const val TIMEOUT_MS = 10_000L
@@ -65,6 +65,7 @@
 
 private const val TEST_SERVICE_ID_1 = 42
 private const val TEST_SERVICE_ID_DUPLICATE = 43
+private const val TEST_SERVICE_ID_2 = 44
 private val TEST_SERVICE_1 = NsdServiceInfo().apply {
     serviceType = "_testservice._tcp"
     serviceName = "MyTestService"
@@ -78,6 +79,13 @@
     port = 12345
 }
 
+private val TEST_SERVICE_1_CUSTOM_HOST = NsdServiceInfo().apply {
+    serviceType = "_testservice._tcp"
+    serviceName = "MyTestService"
+    hostname = "MyTestHost"
+    port = 12345
+}
+
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsInterfaceAdvertiserTest {
@@ -179,7 +187,94 @@
         // Exit announcements finish: the advertiser has no left service and destroys itself
         announceCb.onFinished(testExitInfo)
         thread.waitForIdle(TIMEOUT_MS)
-        verify(cb).onDestroyed(socket)
+        verify(cb).onAllServicesRemoved(socket)
+    }
+
+    @Test
+    fun testAddRemoveServiceWithCustomHost_restartProbingForProbingServices() {
+        val customHost1 = NsdServiceInfo().apply {
+            hostname = "MyTestHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("192.0.2.23"),
+                    parseNumericAddress("2001:db8::1"))
+        }
+        addServiceAndFinishProbing(TEST_SERVICE_ID_1, customHost1)
+        addServiceAndFinishProbing(TEST_SERVICE_ID_2, TEST_SERVICE_1_CUSTOM_HOST)
+        repository.setServiceProbing(TEST_SERVICE_ID_2)
+        val probingInfo = mock(ProbingInfo::class.java)
+        doReturn("MyTestHost")
+                .`when`(repository).getHostnameForServiceId(TEST_SERVICE_ID_1)
+        doReturn(TEST_SERVICE_ID_2).`when`(probingInfo).serviceId
+        doReturn(listOf(probingInfo))
+                .`when`(repository).restartProbingForHostname("MyTestHost")
+        val inOrder = inOrder(prober, announcer)
+
+        // Remove the custom host: the custom host's announcement is stopped and the probing
+        // services which use that hostname are re-announced.
+        advertiser.removeService(TEST_SERVICE_ID_1)
+
+        inOrder.verify(prober).stop(TEST_SERVICE_ID_1)
+        inOrder.verify(announcer).stop(TEST_SERVICE_ID_1)
+        inOrder.verify(prober).stop(TEST_SERVICE_ID_2)
+        inOrder.verify(prober).startProbing(probingInfo)
+    }
+
+    @Test
+    fun testAddRemoveServiceWithCustomHost_restartAnnouncingForProbedServices() {
+        val customHost1 = NsdServiceInfo().apply {
+            hostname = "MyTestHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("192.0.2.23"),
+                    parseNumericAddress("2001:db8::1"))
+        }
+        addServiceAndFinishProbing(TEST_SERVICE_ID_1, customHost1)
+        val announcementInfo =
+                addServiceAndFinishProbing(TEST_SERVICE_ID_2, TEST_SERVICE_1_CUSTOM_HOST)
+        doReturn("MyTestHost")
+                .`when`(repository).getHostnameForServiceId(TEST_SERVICE_ID_1)
+        doReturn(listOf(announcementInfo))
+                .`when`(repository).restartAnnouncingForHostname("MyTestHost")
+        val inOrder = inOrder(prober, announcer)
+
+        // Remove the custom host: the custom host's announcement is stopped and the probed services
+        // which use that hostname are re-announced.
+        advertiser.removeService(TEST_SERVICE_ID_1)
+
+        inOrder.verify(prober).stop(TEST_SERVICE_ID_1)
+        inOrder.verify(announcer).stop(TEST_SERVICE_ID_1)
+        inOrder.verify(announcer).stop(TEST_SERVICE_ID_2)
+        inOrder.verify(announcer).startSending(TEST_SERVICE_ID_2, announcementInfo, 0L /* initialDelayMs */)
+    }
+
+    @Test
+    fun testAddMoreAddressesForCustomHost_restartAnnouncingForProbedServices() {
+        val customHost = NsdServiceInfo().apply {
+            hostname = "MyTestHost"
+            hostAddresses = listOf(
+                parseNumericAddress("192.0.2.23"),
+                parseNumericAddress("2001:db8::1"))
+        }
+        doReturn("MyTestHost")
+            .`when`(repository).getHostnameForServiceId(TEST_SERVICE_ID_1)
+        doReturn("MyTestHost")
+            .`when`(repository).getHostnameForServiceId(TEST_SERVICE_ID_2)
+        val announcementInfo1 =
+            addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1_CUSTOM_HOST)
+
+        val probingInfo2 = addServiceAndStartProbing(TEST_SERVICE_ID_2, customHost)
+        val announcementInfo2 = AnnouncementInfo(TEST_SERVICE_ID_2, emptyList(), emptyList())
+        doReturn(announcementInfo2).`when`(repository).onProbingSucceeded(probingInfo2)
+        doReturn(listOf(announcementInfo1, announcementInfo2))
+            .`when`(repository).restartAnnouncingForHostname("MyTestHost")
+        probeCb.onFinished(probingInfo2)
+
+        val inOrder = inOrder(prober, announcer)
+
+        inOrder.verify(announcer)
+            .startSending(TEST_SERVICE_ID_2, announcementInfo2, 0L /* initialDelayMs */)
+        inOrder.verify(announcer).stop(TEST_SERVICE_ID_1)
+        inOrder.verify(announcer)
+            .startSending(TEST_SERVICE_ID_1, announcementInfo1, 0L /* initialDelayMs */)
     }
 
     @Test
@@ -422,8 +517,8 @@
         verify(prober, never()).startProbing(any())
     }
 
-    private fun addServiceAndFinishProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
-            AnnouncementInfo {
+    private fun addServiceAndStartProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
+            ProbingInfo {
         val testProbingInfo = mock(ProbingInfo::class.java)
         doReturn(serviceId).`when`(testProbingInfo).serviceId
         doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
@@ -432,8 +527,15 @@
         verify(repository).addService(serviceId, serviceInfo, null /* ttl */)
         verify(prober).startProbing(testProbingInfo)
 
+        return testProbingInfo
+    }
+
+    private fun addServiceAndFinishProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
+            AnnouncementInfo {
+        val testProbingInfo = addServiceAndStartProbing(serviceId, serviceInfo)
+
         // Simulate probing success: continues to announcing
-        val testAnnouncementInfo = mock(AnnouncementInfo::class.java)
+        val testAnnouncementInfo = AnnouncementInfo(serviceId, emptyList(), emptyList())
         doReturn(testAnnouncementInfo).`when`(repository).onProbingSucceeded(testProbingInfo)
         probeCb.onFinished(testProbingInfo)
         return testAnnouncementInfo
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index 9474464..4c71991 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -18,11 +18,14 @@
 
 import static com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback;
 import static com.android.server.connectivity.mdns.MulticastPacketReader.PacketHandler;
+import static com.android.testutils.Cleanup.testAndCleanup;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
@@ -34,6 +37,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.util.Log;
 
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.SharedLog;
@@ -47,6 +51,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -55,7 +60,9 @@
 import java.net.DatagramPacket;
 import java.net.NetworkInterface;
 import java.net.SocketException;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -154,7 +161,7 @@
         verify(mSocketCreationCallback).onSocketCreated(tetherSocketKey2);
 
         // Send packet to IPv4 with mSocketKey and verify sending has been called.
-        mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, mSocketKey,
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket).send(ipv4Packet);
@@ -162,7 +169,7 @@
         verify(tetherIfaceSock2, never()).send(any());
 
         // Send packet to IPv4 with onlyUseIpv6OnIpv6OnlyNetworks = true, the packet will be sent.
-        mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, mSocketKey,
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
                 true /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket, times(2)).send(ipv4Packet);
@@ -170,7 +177,7 @@
         verify(tetherIfaceSock2, never()).send(any());
 
         // Send packet to IPv6 with tetherSocketKey1 and verify sending has been called.
-        mSocketClient.sendPacketRequestingMulticastResponse(ipv6Packet, tetherSocketKey1,
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv6Packet), tetherSocketKey1,
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket, never()).send(ipv6Packet);
@@ -180,7 +187,7 @@
         // Send packet to IPv6 with onlyUseIpv6OnIpv6OnlyNetworks = true, the packet will not be
         // sent. Therefore, the tetherIfaceSock1.send() and tetherIfaceSock2.send() are still be
         // called once.
-        mSocketClient.sendPacketRequestingMulticastResponse(ipv6Packet, tetherSocketKey1,
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv6Packet), tetherSocketKey1,
                 true /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket, never()).send(ipv6Packet);
@@ -266,7 +273,7 @@
         verify(mSocketCreationCallback).onSocketCreated(socketKey3);
 
         // Send IPv4 packet on the mSocketKey and verify sending has been called.
-        mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, mSocketKey,
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket).send(ipv4Packet);
@@ -295,7 +302,7 @@
         verify(socketCreationCb2).onSocketCreated(socketKey3);
 
         // Send IPv4 packet on socket2 and verify sending to the socket2 only.
-        mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, socketKey2,
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), socketKey2,
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         // ipv4Packet still sent only once on mSocket: times(1) matches the packet sent earlier on
@@ -309,7 +316,7 @@
         verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback2);
 
         // Send IPv4 packet again and verify it's still sent a second time
-        mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, socketKey2,
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), socketKey2,
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(socket2, times(2)).send(ipv4Packet);
@@ -320,7 +327,7 @@
         verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback);
 
         // Send IPv4 packet and verify no more sending.
-        mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, mSocketKey,
+        mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
         verify(mSocket, times(1)).send(ipv4Packet);
@@ -407,4 +414,61 @@
         verify(creationCallback3).onSocketDestroyed(mSocketKey);
         verify(creationCallback3, never()).onSocketDestroyed(socketKey2);
     }
+
+    @Test
+    public void testSendPacketWithMultipleDatagramPacket() throws IOException {
+        final SocketCallback callback = expectSocketCallback();
+        final List<DatagramPacket> packets = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+            packets.add(new DatagramPacket(new byte[10 + i] /* buff */, 0 /* offset */,
+                    10 + i /* length */, MdnsConstants.IPV4_SOCKET_ADDR));
+        }
+        doReturn(true).when(mSocket).hasJoinedIpv4();
+        doReturn(true).when(mSocket).hasJoinedIpv6();
+        doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+
+        // Notify socket created
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+
+        // Send packets to IPv4 with mSocketKey then verify sending has been called and the
+        // sequence is correct.
+        mSocketClient.sendPacketRequestingMulticastResponse(packets, mSocketKey,
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+        InOrder inOrder = inOrder(mSocket);
+        for (int i = 0; i < 10; i++) {
+            inOrder.verify(mSocket).send(packets.get(i));
+        }
+    }
+
+    @Test
+    public void testSendPacketWithMultiplePacketsWithDifferentAddresses() throws IOException {
+        final SocketCallback callback = expectSocketCallback();
+        final DatagramPacket ipv4Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+                InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+        final DatagramPacket ipv6Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+                InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
+        doReturn(true).when(mSocket).hasJoinedIpv4();
+        doReturn(true).when(mSocket).hasJoinedIpv6();
+        doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+
+        // Notify socket created
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+
+        // Send packets with IPv4 and IPv6 then verify wtf logs and sending has never been called.
+        // Override the default TerribleFailureHandler, as that handler might terminate the process
+        // (if we're on an eng build).
+        final AtomicBoolean hasFailed = new AtomicBoolean(false);
+        final Log.TerribleFailureHandler originalHandler =
+                Log.setWtfHandler((tag, what, system) -> hasFailed.set(true));
+        testAndCleanup(() -> {
+            mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet, ipv6Packet),
+                    mSocketKey, false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+            HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+            assertTrue(hasFailed.get());
+            verify(mSocket, never()).send(any());
+        }, () -> Log.setWtfHandler(originalHandler));
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 8d1dff6..9674da3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -21,11 +21,13 @@
 import android.net.nsd.NsdServiceInfo
 import android.os.Build
 import android.os.HandlerThread
+import com.android.net.module.util.HexDump.hexStringToByteArray
 import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
 import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST
 import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
 import com.android.server.connectivity.mdns.MdnsRecord.TYPE_A
 import com.android.server.connectivity.mdns.MdnsRecord.TYPE_AAAA
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_KEY
 import com.android.server.connectivity.mdns.MdnsRecord.TYPE_PTR
 import com.android.server.connectivity.mdns.MdnsRecord.TYPE_SRV
 import com.android.server.connectivity.mdns.MdnsRecord.TYPE_TXT
@@ -38,6 +40,7 @@
 import java.net.InetSocketAddress
 import java.net.NetworkInterface
 import java.util.Collections
+import java.time.Duration
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -111,6 +114,28 @@
     port = TEST_PORT
 }
 
+private val TEST_SERVICE_CUSTOM_HOST_NO_ADDRESSES = NsdServiceInfo().apply {
+    hostname = "TestHost"
+    hostAddresses = listOf()
+    serviceType = "_testservice._tcp"
+    serviceName = "TestService"
+    port = TEST_PORT
+}
+
+private val TEST_PUBLIC_KEY = hexStringToByteArray(
+        "0201030dc141d0637960b98cbc12cfca" +
+                "221d2879dac26ee5b460e9007c992e19" +
+                "02d897c391b03764d448f7d0c772fdb0" +
+                "3b1d9d6d52ff8886769e8e2362513565" +
+                "270962d3")
+
+private val TEST_PUBLIC_KEY_2 = hexStringToByteArray(
+        "0201030dc141d0637960b98cbc12cfca" +
+                "221d2879dac26ee5b460e9007c992e19" +
+                "02d897c391b03764d448f7d0c772fdb0" +
+                "3b1d9d6d52ff8886769e8e2362513565" +
+                "270962d4")
+
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsRecordRepositoryTest {
@@ -118,10 +143,23 @@
     private val deps = object : Dependencies() {
         override fun getInterfaceInetAddresses(iface: NetworkInterface) =
                 Collections.enumeration(TEST_ADDRESSES.map { it.address })
+
+        override fun elapsedRealTime() = now
+
+        fun elapse(duration: Long) {
+            now += duration
+        }
+
+        fun resetElapsedRealTime() {
+            now = 100
+        }
+
+        var now: Long = 100
     }
 
     @Before
     fun setUp() {
+        deps.resetElapsedRealTime()
         thread.start()
     }
 
@@ -134,11 +172,13 @@
     private fun makeFlags(
         includeInetAddressesInProbing: Boolean = false,
         isKnownAnswerSuppressionEnabled: Boolean = false,
-        unicastReplyEnabled: Boolean = true
+        unicastReplyEnabled: Boolean = true,
+        avoidAdvertisingEmptyTxtRecords: Boolean = true
     ) = MdnsFeatureFlags.Builder()
         .setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
         .setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
         .setIsUnicastReplyEnabled(unicastReplyEnabled)
+        .setAvoidAdvertisingEmptyTxtRecords(avoidAdvertisingEmptyTxtRecords)
         .build()
 
     @Test
@@ -146,7 +186,7 @@
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         assertEquals(0, repository.servicesCount)
         assertEquals(-1,
-                repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
+                repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, Duration.ofSeconds(50)))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -169,7 +209,7 @@
         assertEquals(MdnsServiceRecord(expectedName,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                SHORT_TTL /* ttlMillis */,
+                50_000L /* ttlMillis */,
                 0 /* servicePriority */, 0 /* serviceWeight */,
                 TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
 
@@ -559,6 +599,7 @@
             TYPE_PTR -> return MdnsPointerRecord(name, false /* isUnicast */)
             TYPE_SRV -> return MdnsServiceRecord(name, false /* isUnicast */)
             TYPE_TXT -> return MdnsTextRecord(name, false /* isUnicast */)
+            TYPE_KEY -> return MdnsKeyRecord(name, false /* isUnicast */)
             TYPE_A, TYPE_AAAA -> return MdnsInetAddressRecord(name, type, false /* isUnicast */)
             else -> fail("Unexpected question type: $type")
         }
@@ -886,6 +927,159 @@
     }
 
     @Test
+    fun testGetReply_keyQuestionForServiceName_returnsKeyRecord() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            serviceType = "_testservice._tcp"
+            serviceName = "MyTestService1"
+            port = TEST_PORT
+            publicKey = TEST_PUBLIC_KEY
+        })
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+            serviceType = "_testservice._tcp"
+            serviceName = "MyTestService2"
+            port = 0 // No SRV RR
+            publicKey = TEST_PUBLIC_KEY
+        })
+        val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+        val serviceName1 = arrayOf("MyTestService1", "_testservice", "_tcp", "local")
+        val serviceName2 = arrayOf("MyTestService2", "_testservice", "_tcp", "local")
+
+        val query1 = makeQuery(TYPE_KEY to serviceName1)
+        val reply1 = repository.getReply(query1, src)
+
+        assertNotNull(reply1)
+        assertEquals(listOf(MdnsKeyRecord(serviceName1,
+                0, false, LONG_TTL, TEST_PUBLIC_KEY)),
+                reply1.answers)
+        assertEquals(listOf(),
+                reply1.additionalAnswers)
+
+        val query2 = makeQuery(TYPE_KEY to serviceName2)
+        val reply2 = repository.getReply(query2, src)
+
+        assertNotNull(reply2)
+        assertEquals(listOf(MdnsKeyRecord(serviceName2,
+                0, false, LONG_TTL, TEST_PUBLIC_KEY)),
+                reply2.answers)
+        assertEquals(listOf(MdnsNsecRecord(serviceName2,
+                0L, true, SHORT_TTL,
+                serviceName2 /* nextDomain */,
+                intArrayOf(TYPE_KEY))),
+                reply2.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_keyQuestionForHostname_returnsKeyRecord() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            hostname = "MyHost1"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("2001:db8::2"))
+            publicKey = TEST_PUBLIC_KEY
+        })
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+            hostname = "MyHost2"
+            hostAddresses = listOf() // No address records
+            publicKey = TEST_PUBLIC_KEY
+        })
+        val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+        val hostname1 = arrayOf("MyHost1", "local")
+        val hostname2 = arrayOf("MyHost2", "local")
+
+        val query1 = makeQuery(TYPE_KEY to hostname1)
+        val reply1 = repository.getReply(query1, src)
+
+        assertNotNull(reply1)
+        assertEquals(listOf(MdnsKeyRecord(hostname1,
+                0, false, LONG_TTL, TEST_PUBLIC_KEY)),
+                reply1.answers)
+        assertEquals(listOf(),
+                reply1.additionalAnswers)
+
+        val query2 = makeQuery(TYPE_KEY to hostname2)
+        val reply2 = repository.getReply(query2, src)
+
+        assertNotNull(reply2)
+        assertEquals(listOf(MdnsKeyRecord(hostname2,
+                0, false, LONG_TTL, TEST_PUBLIC_KEY)),
+                reply2.answers)
+        assertEquals(listOf(MdnsNsecRecord(hostname2, 0L, true, SHORT_TTL,
+                hostname2 /* nextDomain */,
+                intArrayOf(TYPE_KEY))),
+                reply2.additionalAnswers)
+    }
+
+    @Test
+    fun testGetReply_keyRecordForHostRemoved_noAnswertoKeyQuestion() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            hostname = "MyHost1"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("2001:db8::2"))
+            publicKey = TEST_PUBLIC_KEY
+        })
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+            hostname = "MyHost2"
+            hostAddresses = listOf() // No address records
+            publicKey = TEST_PUBLIC_KEY
+        })
+        repository.removeService(TEST_SERVICE_ID_1)
+        repository.removeService(TEST_SERVICE_ID_2)
+        val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+        val hostname1 = arrayOf("MyHost1", "local")
+        val hostname2 = arrayOf("MyHost2", "local")
+
+        val query1 = makeQuery(TYPE_KEY to hostname1)
+        val reply1 = repository.getReply(query1, src)
+
+        assertNull(reply1)
+
+        val query2 = makeQuery(TYPE_KEY to hostname2)
+        val reply2 = repository.getReply(query2, src)
+
+        assertNull(reply2)
+    }
+
+    @Test
+    fun testGetReply_keyRecordForServiceRemoved_noAnswertoKeyQuestion() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            serviceType = "_testservice._tcp"
+            serviceName = "MyTestService1"
+            port = TEST_PORT
+            publicKey = TEST_PUBLIC_KEY
+        })
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+            serviceType = "_testservice._tcp"
+            serviceName = "MyTestService2"
+            port = 0 // No SRV RR
+            publicKey = TEST_PUBLIC_KEY
+        })
+        repository.removeService(TEST_SERVICE_ID_1)
+        repository.removeService(TEST_SERVICE_ID_2)
+        val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+        val serviceName1 = arrayOf("MyTestService1", "_testservice", "_tcp", "local")
+        val serviceName2 = arrayOf("MyTestService2", "_testservice", "_tcp", "local")
+
+        val query1 = makeQuery(TYPE_KEY to serviceName1)
+        val reply1 = repository.getReply(query1, src)
+
+        assertNull(reply1)
+
+        val query2 = makeQuery(TYPE_KEY to serviceName2)
+        val reply2 = repository.getReply(query2, src)
+
+        assertNull(reply2)
+    }
+
+    @Test
     fun testGetReply_customHostRemoved_noAnswerToAAAAQuestion() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(
@@ -989,6 +1183,102 @@
     }
 
     @Test
+    fun testGetReply_ipv4AndIpv6Queries_ipv4AndIpv6Replies() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val replyIpv4 = repository.getReply(query, srcIpv4)
+        val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+        val replyIpv6 = repository.getReply(query, srcIpv6)
+
+        assertNotNull(replyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), replyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, replyIpv4.destination.port)
+        assertNotNull(replyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), replyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, replyIpv6.destination.port)
+    }
+
+    @Test
+    fun testGetReply_twoIpv4QueriesInOneSecond_theSecondReplyIsThrottled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val firstReplyIpv4 = repository.getReply(query, srcIpv4)
+        deps.elapse(500L)
+        val secondReply = repository.getReply(query, srcIpv4)
+
+        assertNotNull(firstReplyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), firstReplyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, firstReplyIpv4.destination.port)
+        assertNull(secondReply)
+    }
+
+
+    @Test
+    fun testGetReply_twoIpv6QueriesInOneSecond_theSecondReplyIsThrottled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+        val firstReplyIpv6 = repository.getReply(query, srcIpv6)
+        deps.elapse(500L)
+        val secondReply = repository.getReply(query, srcIpv6)
+
+        assertNotNull(firstReplyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), firstReplyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, firstReplyIpv6.destination.port)
+        assertNull(secondReply)
+    }
+
+    @Test
+    fun testGetReply_twoIpv4QueriesInMoreThanOneSecond_repliesAreNotThrottled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val firstReplyIpv4 = repository.getReply(query, srcIpv4)
+        // The longest possible interval that may make the reply throttled is
+        // 1000 (MIN_MULTICAST_REPLY_INTERVAL_MS) + 120 (delay for shared name) = 1120
+        deps.elapse(1121L)
+        val secondReplyIpv4 = repository.getReply(query, srcIpv4)
+
+        assertNotNull(firstReplyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), firstReplyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, firstReplyIpv4.destination.port)
+        assertNotNull(secondReplyIpv4)
+        assertEquals(MdnsConstants.getMdnsIPv4Address(), secondReplyIpv4.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, secondReplyIpv4.destination.port)
+    }
+
+    @Test
+    fun testGetReply_twoIpv6QueriesInMoreThanOneSecond_repliesAreNotThrottled() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+        val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+        val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+        val firstReplyIpv6 = repository.getReply(query, srcIpv6)
+        // The longest possible interval that may make the reply throttled is
+        // 1000 (MIN_MULTICAST_REPLY_INTERVAL_MS) + 120 (delay for shared name) = 1120
+        deps.elapse(1121L)
+        val secondReplyIpv6 = repository.getReply(query, srcIpv6)
+
+        assertNotNull(firstReplyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), firstReplyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, firstReplyIpv6.destination.port)
+        assertNotNull(secondReplyIpv6)
+        assertEquals(MdnsConstants.getMdnsIPv6Address(), secondReplyIpv6.destination.address)
+        assertEquals(MdnsConstants.MDNS_PORT, secondReplyIpv6.destination.port)
+    }
+
+    @Test
     fun testGetConflictingServices() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
@@ -1103,8 +1393,8 @@
     @Test
     fun testGetConflictingServices_customHostsReplyHasFewerAddressesThanUs_noConflict() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
 
         val packet = MdnsPacket(
                 0, /* flags */
@@ -1122,10 +1412,30 @@
     }
 
     @Test
-    fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() {
+    fun testGetConflictingServices_customHostsReplyHasSameNameRecord_conflictDuringProbing() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+
+        val packet = MdnsPacket(
+            0, /* flags */
+            emptyList(), /* questions */
+            listOf(MdnsKeyRecord(arrayOf("TestHost", "local"),
+                    0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    0L /* ttlMillis */, TEST_PUBLIC_KEY),
+            ) /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */)
+
+        assertEquals(mapOf(TEST_CUSTOM_HOST_ID_1 to CONFLICT_HOST),
+            repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
 
         val packet = MdnsPacket(
                 0, /* flags */
@@ -1149,8 +1459,8 @@
     @Test
     fun testGetConflictingServices_customHostsCaseInsensitiveReplyHasIdenticalHosts_noConflict() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
+        repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
 
         val packet = MdnsPacket(
                 0, /* flags */
@@ -1171,6 +1481,190 @@
     }
 
     @Test
+    fun testGetConflictingServices_identicalKeyRecordsForService_noConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            serviceType = "_testservice._tcp"
+            serviceName = "MyTestService"
+            port = TEST_PORT
+            publicKey = TEST_PUBLIC_KEY
+        }, null /* ttl */)
+
+        val otherTtlMillis = 1234L
+        val packet = MdnsPacket(
+                0 /* flags */,
+                emptyList() /* questions */,
+                listOf(
+                        MdnsKeyRecord(
+                                arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                otherTtlMillis,
+                                TEST_PUBLIC_KEY)
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_differentKeyRecordsForService_conflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            serviceType = "_testservice._tcp"
+            serviceName = "MyTestService"
+            port = TEST_PORT
+            publicKey = TEST_PUBLIC_KEY
+        }, null /* null */)
+
+        val otherTtlMillis = 1234L
+        val packet = MdnsPacket(
+                0 /* flags */,
+                emptyList() /* questions */,
+                listOf(
+                        MdnsKeyRecord(
+                                arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                otherTtlMillis,
+                                TEST_PUBLIC_KEY_2)
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(mapOf(TEST_SERVICE_ID_1 to CONFLICT_SERVICE),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_identicalKeyRecordsForHost_noConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            hostname = "MyHost"
+            hostAddresses = listOf(
+                parseNumericAddress("2001:db8::1"),
+                parseNumericAddress("2001:db8::2")
+            )
+            publicKey = TEST_PUBLIC_KEY
+        })
+
+        val otherTtlMillis = 1234L
+        val packet = MdnsPacket(
+                0 /* flags */,
+                emptyList() /* questions */,
+                listOf(
+                        MdnsKeyRecord(
+                                arrayOf("MyHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                otherTtlMillis,
+                                TEST_PUBLIC_KEY)
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(emptyMap(),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_keyForCustomHostReplySameRecordName_conflictDuringProbing() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            hostname = "MyHost"
+            publicKey = TEST_PUBLIC_KEY
+        }, null /* ttl */)
+
+        val otherTtlMillis = 1234L
+        val packet = MdnsPacket(
+            0 /* flags */,
+            emptyList() /* questions */,
+            listOf(MdnsInetAddressRecord(arrayOf("MyHost", "local"),
+                    0L /* receiptTimeMillis */,
+                    true /* cacheFlush */,
+                    otherTtlMillis,
+                    parseNumericAddress("192.168.2.111"))
+            ) /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */
+        )
+
+        assertEquals(mapOf(TEST_SERVICE_ID_1 to CONFLICT_HOST),
+            repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_differentKeyRecordsForHost_conflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            hostname = "MyHost"
+            hostAddresses = listOf(
+                    parseNumericAddress("2001:db8::1"),
+                    parseNumericAddress("2001:db8::2"))
+            publicKey = TEST_PUBLIC_KEY
+        }, null /* ttl */)
+
+        val otherTtlMillis = 1234L
+        val packet = MdnsPacket(
+                0 /* flags */,
+                emptyList() /* questions */,
+                listOf(
+                        MdnsKeyRecord(
+                                arrayOf("MyHost", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                otherTtlMillis,
+                                TEST_PUBLIC_KEY_2)
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(mapOf(TEST_SERVICE_ID_1 to CONFLICT_HOST),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_multipleRegistrationsForHostKey_noConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, NsdServiceInfo().apply {
+            hostname = "MyHost"
+            hostAddresses = listOf(
+                parseNumericAddress("2001:db8::1"),
+                parseNumericAddress("2001:db8::2"))
+            publicKey = TEST_PUBLIC_KEY
+        })
+        repository.addService(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
+            serviceType = "_testservice._tcp"
+            serviceName = "MyTestService"
+            port = TEST_PORT
+            hostname = "MyHost"
+            publicKey = TEST_PUBLIC_KEY
+        }, null /* ttl */)
+
+        // Although there's a KEY RR in the second registration being probed, it shouldn't conflict
+        // with an address record which is from a probed registration in the repository.
+        val otherTtlMillis = 1234L
+        val packet = MdnsPacket(
+            0 /* flags */,
+            emptyList() /* questions */,
+            listOf(
+                MdnsInetAddressRecord(
+                    arrayOf("MyHost", "local"),
+                    0L /* receiptTimeMillis */, true /* cacheFlush */,
+                    otherTtlMillis,
+                    parseNumericAddress("2001:db8::1"))
+            ) /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */)
+
+        assertEquals(mapOf(), repository.getConflictingServices(packet))
+    }
+
+    @Test
     fun testGetConflictingServices_IdenticalService() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
@@ -1229,6 +1723,30 @@
     }
 
     @Test
+    fun testGetConflictingServices_ZeroLengthTxtRecord_NoConflict() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        val packet = MdnsPacket(
+            0 /* flags */,
+            emptyList() /* questions */,
+            listOf(
+                    MdnsTextRecord(
+                        arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        SHORT_TTL,
+                        listOf(TextEntry("", null as ByteArray?))
+                    ),
+            ) /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */
+        )
+
+        assertEquals(emptyMap(), repository.getConflictingServices(packet))
+    }
+
+    @Test
     fun testGetServiceRepliedRequestsCount() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
@@ -1675,6 +2193,167 @@
         assertEquals(0, reply.additionalAnswers.size)
         assertEquals(knownAnswers, reply.knownAnswers)
     }
+
+    private fun doAddServiceWithEmptyTxtRecordTest(flags: MdnsFeatureFlags): MdnsTextRecord {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+        repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        val questions = listOf(MdnsTextRecord(
+            arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+            true /* isUnicast */
+        ))
+        val query = MdnsPacket(
+            0 /* flags */,
+            questions,
+            emptyList() /* answers */,
+            emptyList() /* authorityRecords */,
+            emptyList() /* additionalRecords */
+        )
+        val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+        val reply = repository.getReply(query, src)
+
+        assertNotNull(reply)
+        assertEquals(1, reply.answers.size)
+        assertTrue(reply.answers[0] is MdnsTextRecord)
+        return reply.answers[0] as MdnsTextRecord
+    }
+
+    @Test
+    fun testAddService_AvoidEmptyTxtRecords_HasTxtRecordWithEmptyString() {
+        val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags())
+        assertEquals(1, answerRecord.entries.size)
+        assertEquals(0, answerRecord.entries[0].key.length)
+        assertNull(answerRecord.entries[0].value)
+    }
+
+    @Test
+    fun testAddService_UsesEmptyTxtRecords_HasEmptyTxtRecord() {
+        val answerRecord = doAddServiceWithEmptyTxtRecordTest(makeFlags(
+            avoidAdvertisingEmptyTxtRecords = false
+        ))
+        assertEquals(0, answerRecord.entries.size)
+    }
+
+    @Test
+    fun testRestartProbingForHostname() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1,
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+        repository.addService(TEST_SERVICE_CUSTOM_HOST_ID_1,
+                TEST_SERVICE_CUSTOM_HOST_NO_ADDRESSES, null)
+        repository.setServiceProbing(TEST_SERVICE_CUSTOM_HOST_ID_1)
+        repository.removeService(TEST_CUSTOM_HOST_ID_1)
+
+        val probingInfos = repository.restartProbingForHostname("TestHost")
+
+        assertEquals(1, probingInfos.size)
+        val probingInfo = probingInfos.get(0)
+        assertEquals(TEST_SERVICE_CUSTOM_HOST_ID_1, probingInfo.serviceId)
+        val packet = probingInfo.getPacket(0)
+        assertEquals(0, packet.transactionId)
+        assertEquals(MdnsConstants.FLAGS_QUERY, packet.flags)
+        assertEquals(0, packet.answers.size)
+        assertEquals(0, packet.additionalRecords.size)
+        assertEquals(1, packet.questions.size)
+        val serviceName = arrayOf("TestService", "_testservice", "_tcp", "local")
+        assertEquals(MdnsAnyRecord(serviceName, false /* unicast */), packet.questions[0])
+        assertThat(packet.authorityRecords).containsExactly(
+                MdnsServiceRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        false /* cacheFlush */,
+                        SHORT_TTL /* ttlMillis */,
+                        0 /* servicePriority */,
+                        0 /* serviceWeight */,
+                        TEST_PORT,
+                        TEST_CUSTOM_HOST_1_NAME))
+    }
+
+    @Test
+    fun testRestartAnnouncingForHostname() {
+        val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+        repository.initWithService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1,
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+        repository.addServiceAndFinishProbing(TEST_SERVICE_CUSTOM_HOST_ID_1,
+                TEST_SERVICE_CUSTOM_HOST_NO_ADDRESSES)
+        repository.removeService(TEST_CUSTOM_HOST_ID_1)
+
+        val announcementInfos = repository.restartAnnouncingForHostname("TestHost")
+
+        assertEquals(1, announcementInfos.size)
+        val announcementInfo = announcementInfos.get(0)
+        assertEquals(TEST_SERVICE_CUSTOM_HOST_ID_1, announcementInfo.serviceId)
+        val packet = announcementInfo.getPacket(0)
+        assertEquals(0, packet.transactionId)
+        assertEquals(0x8400 /* response, authoritative */, packet.flags)
+        assertEquals(0, packet.questions.size)
+        assertEquals(0, packet.authorityRecords.size)
+        val serviceName = arrayOf("TestService", "_testservice", "_tcp", "local")
+        val serviceType = arrayOf("_testservice", "_tcp", "local")
+        val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
+        val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
+        val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address)
+        assertThat(packet.answers).containsExactly(
+                MdnsPointerRecord(
+                        serviceType,
+                        0L /* receiptTimeMillis */,
+                        // Not a unique name owned by the announcer, so cacheFlush=false
+                        false /* cacheFlush */,
+                        4500000L /* ttlMillis */,
+                        serviceName),
+                MdnsServiceRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        120000L /* ttlMillis */,
+                        0 /* servicePriority */,
+                        0 /* serviceWeight */,
+                        TEST_PORT /* servicePort */,
+                        TEST_CUSTOM_HOST_1_NAME),
+                MdnsTextRecord(
+                        serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        4500000L /* ttlMillis */,
+                        emptyList() /* entries */),
+                MdnsPointerRecord(
+                        arrayOf("_services", "_dns-sd", "_udp", "local"),
+                        0L /* receiptTimeMillis */,
+                        false /* cacheFlush */,
+                        4500000L /* ttlMillis */,
+                        serviceType))
+        assertThat(packet.additionalRecords).containsExactly(
+                MdnsNsecRecord(v4AddrRev,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        120000L /* ttlMillis */,
+                        v4AddrRev,
+                        intArrayOf(TYPE_PTR)),
+                MdnsNsecRecord(TEST_HOSTNAME,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        120000L /* ttlMillis */,
+                        TEST_HOSTNAME,
+                        intArrayOf(TYPE_A, TYPE_AAAA)),
+                MdnsNsecRecord(v6Addr1Rev,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        120000L /* ttlMillis */,
+                        v6Addr1Rev,
+                        intArrayOf(TYPE_PTR)),
+                MdnsNsecRecord(v6Addr2Rev,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        120000L /* ttlMillis */,
+                        v6Addr2Rev,
+                        intArrayOf(TYPE_PTR)),
+                MdnsNsecRecord(serviceName,
+                        0L /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        4500000L /* ttlMillis */,
+                        serviceName,
+                        intArrayOf(TYPE_TXT, TYPE_SRV)))
+    }
 }
 
 private fun MdnsRecordRepository.initWithService(
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
index 55c2846..784c502 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -16,16 +16,20 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsConstants.QCLASS_INTERNET;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
+import static java.util.Collections.emptyList;
+
 import android.util.Log;
 
 import com.android.net.module.util.HexDump;
@@ -370,6 +374,30 @@
         assertEquals(dataInText, dataOutText);
     }
 
+    private static MdnsTextRecord makeTextRecordWithEntries(List<TextEntry> entries) {
+        return new MdnsTextRecord(new String[] { "test", "record" }, 0L /* receiptTimeMillis */,
+                true /* cacheFlush */, 120_000L /* ttlMillis */, entries);
+    }
+
+    @Test
+    public void testTextRecord_EmptyRecordsAreEquivalent() {
+        final MdnsTextRecord record1 = makeTextRecordWithEntries(emptyList());
+        final MdnsTextRecord record2 = makeTextRecordWithEntries(
+                List.of(new TextEntry("", (byte[]) null)));
+        final MdnsTextRecord record3 = makeTextRecordWithEntries(
+                List.of(new TextEntry(null, (byte[]) null)));
+        final MdnsTextRecord nonEmptyRecord = makeTextRecordWithEntries(
+                List.of(new TextEntry("a", (byte[]) null)));
+
+        assertEquals(record1, record1);
+        assertEquals(record1, record2);
+        assertEquals(record1, record3);
+
+        assertNotEquals(nonEmptyRecord, record1);
+        assertNotEquals(nonEmptyRecord, record2);
+        assertNotEquals(nonEmptyRecord, record3);
+    }
+
     private static String toHex(MdnsRecord record) throws IOException {
         MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
         record.write(writer, record.getReceiptTime());
@@ -424,4 +452,92 @@
         assertEquals(new TextEntry("xyz", HexDump.hexStringToByteArray("FFEFDFCF")),
                 entries.get(2));
     }
+
+    @Test
+    public void testKeyRecord() throws IOException {
+        final byte[] dataIn =
+                HexDump.hexStringToByteArray(
+                        "09746573742d686f7374056c6f63616c"
+                                + "00001980010000000a00440201030dc1"
+                                + "41d0637960b98cbc12cfca221d2879da"
+                                + "c26ee5b460e9007c992e1902d897c391"
+                                + "b03764d448f7d0c772fdb03b1d9d6d52"
+                                + "ff8886769e8e2362513565270962d3");
+        final byte[] rData =
+                HexDump.hexStringToByteArray(
+                        "0201030dc141d0637960b98cbc12cfca"
+                                + "221d2879dac26ee5b460e9007c992e19"
+                                + "02d897c391b03764d448f7d0c772fdb0"
+                                + "3b1d9d6d52ff8886769e8e2362513565"
+                                + "270962d3");
+        assertNotNull(dataIn);
+        String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+        // Decode
+        DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+        MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+        String[] name = reader.readLabels();
+        assertNotNull(name);
+        assertEquals(2, name.length);
+        String fqdn = MdnsRecord.labelsToString(name);
+        assertEquals("test-host.local", fqdn);
+
+        int type = reader.readUInt16();
+        assertEquals(MdnsRecord.TYPE_KEY, type);
+
+        MdnsKeyRecord keyRecord;
+
+        // MdnsKeyRecord(String[] name, MdnsPacketReader reader)
+        reader = new MdnsPacketReader(packet);
+        reader.readLabels(); // Skip labels
+        reader.readUInt16(); // Skip type
+        keyRecord = new MdnsKeyRecord(name, reader);
+        assertEquals(MdnsRecord.TYPE_KEY, keyRecord.getType());
+        assertTrue(keyRecord.getTtl() > 0); // Not a question so the TTL is greater than 0
+        assertTrue(keyRecord.getCacheFlush());
+        assertArrayEquals(new String[] {"test-host", "local"}, keyRecord.getName());
+        assertArrayEquals(rData, keyRecord.getRData());
+        assertNotEquals(rData, keyRecord.getRData()); // Uses a copy of the original RDATA
+        assertEquals(dataInText, toHex(keyRecord));
+
+        // MdnsKeyRecord(String[] name, MdnsPacketReader reader, boolean isQuestion)
+        reader = new MdnsPacketReader(packet);
+        reader.readLabels(); // Skip labels
+        reader.readUInt16(); // Skip type
+        keyRecord = new MdnsKeyRecord(name, reader, false /* isQuestion */);
+        assertEquals(MdnsRecord.TYPE_KEY, keyRecord.getType());
+        assertTrue(keyRecord.getTtl() > 0); // Not a question, so the TTL is greater than 0
+        assertTrue(keyRecord.getCacheFlush());
+        assertArrayEquals(new String[] {"test-host", "local"}, keyRecord.getName());
+        assertArrayEquals(rData, keyRecord.getRData());
+        assertNotEquals(rData, keyRecord.getRData()); // Uses a copy of the original RDATA
+
+        // MdnsKeyRecord(String[] name, boolean isUnicast)
+        keyRecord = new MdnsKeyRecord(name, false /* isUnicast */);
+        assertEquals(MdnsRecord.TYPE_KEY, keyRecord.getType());
+        assertEquals(0, keyRecord.getTtl());
+        assertEquals(QCLASS_INTERNET, keyRecord.getRecordClass());
+        assertFalse(keyRecord.getCacheFlush());
+        assertArrayEquals(new String[] {"test-host", "local"}, keyRecord.getName());
+        assertArrayEquals(null, keyRecord.getRData());
+
+        // MdnsKeyRecord(String[] name, long receiptTimeMillis, boolean cacheFlush, long ttlMillis,
+        // byte[] rData)
+        keyRecord =
+                new MdnsKeyRecord(
+                        name,
+                        10 /* receiptTimeMillis */,
+                        true /* cacheFlush */,
+                        20_000 /* ttlMillis */,
+                        rData);
+        assertEquals(MdnsRecord.TYPE_KEY, keyRecord.getType());
+        assertEquals(10, keyRecord.getReceiptTime());
+        assertTrue(keyRecord.getCacheFlush());
+        assertEquals(20_000, keyRecord.getTtl());
+        assertEquals(QCLASS_INTERNET, keyRecord.getRecordClass());
+        assertArrayEquals(new String[] {"test-host", "local"}, keyRecord.getName());
+        assertArrayEquals(rData, keyRecord.getRData());
+        assertNotEquals(rData, keyRecord.getRData()); // Uses a copy of the original RDATA
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index b040ab6..0a8f108 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -154,6 +154,11 @@
         serviceCache.registerServiceExpiredCallback(cacheKey, callback)
     }
 
+    private fun removeServices(
+            serviceCache: MdnsServiceCache,
+            cacheKey: CacheKey
+    ): Unit = runningOnHandlerAndReturn { serviceCache.removeServices(cacheKey) }
+
     @Test
     fun testAddAndRemoveService() {
         val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
@@ -291,6 +296,37 @@
         assertEquals(response4, responses[3])
     }
 
+    @Test
+    fun testRemoveServices() {
+        val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+        addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
+        addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_1, SERVICE_TYPE_2))
+        val responses1 = getServices(serviceCache, cacheKey1)
+        assertEquals(2, responses1.size)
+        assertTrue(responses1.stream().anyMatch { response ->
+            response.serviceInstanceName == SERVICE_NAME_1
+        })
+        assertTrue(responses1.any { response ->
+            response.serviceInstanceName == SERVICE_NAME_2
+        })
+        val responses2 = getServices(serviceCache, cacheKey2)
+        assertEquals(1, responses2.size)
+        assertTrue(responses2.stream().anyMatch { response ->
+            response.serviceInstanceName == SERVICE_NAME_1
+        })
+
+        removeServices(serviceCache, cacheKey1)
+        val responses3 = getServices(serviceCache, cacheKey1)
+        assertEquals(0, responses3.size)
+        val responses4 = getServices(serviceCache, cacheKey2)
+        assertEquals(1, responses4.size)
+
+        removeServices(serviceCache, cacheKey2)
+        val responses5 = getServices(serviceCache, cacheKey2)
+        assertEquals(0, responses5.size)
+    }
+
     private fun createResponse(
             serviceInstanceName: String,
             serviceType: String,
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index 8740e80..4ce8ba6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -54,7 +54,8 @@
                         "192.168.1.1",
                         "2001::1",
                         List.of("vn=Google Inc.", "mn=Google Nest Hub Max"),
-                        /* textEntries= */ null);
+                        /* textEntries= */ null,
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertTrue(info.getAttributeByKey("vn").equals("Google Inc."));
         assertTrue(info.getAttributeByKey("mn").equals("Google Nest Hub Max"));
@@ -73,7 +74,8 @@
                         "2001::1",
                         /* textStrings= */ null,
                         List.of(MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
-                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")),
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertTrue(info.getAttributeByKey("vn").equals("Google Inc."));
         assertTrue(info.getAttributeByKey("mn").equals("Google Nest Hub Max"));
@@ -93,7 +95,8 @@
                         List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
                         List.of(
                                 MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
-                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")),
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertEquals(Map.of("vn", "Google Inc.", "mn", "Google Nest Hub Max"),
                 info.getAttributes());
@@ -113,7 +116,8 @@
                         List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
                         List.of(MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
                                 MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
-                                MdnsServiceInfo.TextEntry.fromString("mn=Google WiFi Router")));
+                                MdnsServiceInfo.TextEntry.fromString("mn=Google WiFi Router")),
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertEquals(Map.of("vn", "Google Inc.", "mn", "Google Nest Hub Max"),
                 info.getAttributes());
@@ -131,7 +135,8 @@
                         "192.168.1.1",
                         "2001::1",
                         List.of("KEY=Value"),
-                        /* textEntries= */ null);
+                        /* textEntries= */ null,
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertEquals("Value", info.getAttributeByKey("key"));
         assertEquals("Value", info.getAttributeByKey("KEY"));
@@ -150,7 +155,9 @@
                         12345,
                         "192.168.1.1",
                         "2001::1",
-                        List.of());
+                        List.of(),
+                        /* textEntries= */ null,
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertEquals(info.getInterfaceIndex(), INTERFACE_INDEX_UNSPECIFIED);
     }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 09236b1..569f4d7 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -38,6 +38,7 @@
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
@@ -117,8 +118,6 @@
     @Mock
     private MdnsServiceBrowserListener mockListenerTwo;
     @Mock
-    private MdnsPacketWriter mockPacketWriter;
-    @Mock
     private MdnsMultinetworkSocketClient mockSocketClient;
     @Mock
     private Network mockNetwork;
@@ -145,6 +144,7 @@
     private long latestDelayMs = 0;
     private Message delayMessage = null;
     private Handler realHandler = null;
+    private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
 
     @Before
     @SuppressWarnings("DoNotMock")
@@ -162,57 +162,59 @@
             expectedIPv6Packets[i] = new DatagramPacket(buf, 0 /* offset */, 5 /* length */,
                     MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
         }
-        when(mockPacketWriter.getPacket(IPV4_ADDRESS))
-                .thenReturn(expectedIPv4Packets[0])
-                .thenReturn(expectedIPv4Packets[1])
-                .thenReturn(expectedIPv4Packets[2])
-                .thenReturn(expectedIPv4Packets[3])
-                .thenReturn(expectedIPv4Packets[4])
-                .thenReturn(expectedIPv4Packets[5])
-                .thenReturn(expectedIPv4Packets[6])
-                .thenReturn(expectedIPv4Packets[7])
-                .thenReturn(expectedIPv4Packets[8])
-                .thenReturn(expectedIPv4Packets[9])
-                .thenReturn(expectedIPv4Packets[10])
-                .thenReturn(expectedIPv4Packets[11])
-                .thenReturn(expectedIPv4Packets[12])
-                .thenReturn(expectedIPv4Packets[13])
-                .thenReturn(expectedIPv4Packets[14])
-                .thenReturn(expectedIPv4Packets[15])
-                .thenReturn(expectedIPv4Packets[16])
-                .thenReturn(expectedIPv4Packets[17])
-                .thenReturn(expectedIPv4Packets[18])
-                .thenReturn(expectedIPv4Packets[19])
-                .thenReturn(expectedIPv4Packets[20])
-                .thenReturn(expectedIPv4Packets[21])
-                .thenReturn(expectedIPv4Packets[22])
-                .thenReturn(expectedIPv4Packets[23]);
+        when(mockDeps.getDatagramPacketsFromMdnsPacket(
+                any(), any(MdnsPacket.class), eq(IPV4_ADDRESS), anyBoolean()))
+                .thenReturn(List.of(expectedIPv4Packets[0]))
+                .thenReturn(List.of(expectedIPv4Packets[1]))
+                .thenReturn(List.of(expectedIPv4Packets[2]))
+                .thenReturn(List.of(expectedIPv4Packets[3]))
+                .thenReturn(List.of(expectedIPv4Packets[4]))
+                .thenReturn(List.of(expectedIPv4Packets[5]))
+                .thenReturn(List.of(expectedIPv4Packets[6]))
+                .thenReturn(List.of(expectedIPv4Packets[7]))
+                .thenReturn(List.of(expectedIPv4Packets[8]))
+                .thenReturn(List.of(expectedIPv4Packets[9]))
+                .thenReturn(List.of(expectedIPv4Packets[10]))
+                .thenReturn(List.of(expectedIPv4Packets[11]))
+                .thenReturn(List.of(expectedIPv4Packets[12]))
+                .thenReturn(List.of(expectedIPv4Packets[13]))
+                .thenReturn(List.of(expectedIPv4Packets[14]))
+                .thenReturn(List.of(expectedIPv4Packets[15]))
+                .thenReturn(List.of(expectedIPv4Packets[16]))
+                .thenReturn(List.of(expectedIPv4Packets[17]))
+                .thenReturn(List.of(expectedIPv4Packets[18]))
+                .thenReturn(List.of(expectedIPv4Packets[19]))
+                .thenReturn(List.of(expectedIPv4Packets[20]))
+                .thenReturn(List.of(expectedIPv4Packets[21]))
+                .thenReturn(List.of(expectedIPv4Packets[22]))
+                .thenReturn(List.of(expectedIPv4Packets[23]));
 
-        when(mockPacketWriter.getPacket(IPV6_ADDRESS))
-                .thenReturn(expectedIPv6Packets[0])
-                .thenReturn(expectedIPv6Packets[1])
-                .thenReturn(expectedIPv6Packets[2])
-                .thenReturn(expectedIPv6Packets[3])
-                .thenReturn(expectedIPv6Packets[4])
-                .thenReturn(expectedIPv6Packets[5])
-                .thenReturn(expectedIPv6Packets[6])
-                .thenReturn(expectedIPv6Packets[7])
-                .thenReturn(expectedIPv6Packets[8])
-                .thenReturn(expectedIPv6Packets[9])
-                .thenReturn(expectedIPv6Packets[10])
-                .thenReturn(expectedIPv6Packets[11])
-                .thenReturn(expectedIPv6Packets[12])
-                .thenReturn(expectedIPv6Packets[13])
-                .thenReturn(expectedIPv6Packets[14])
-                .thenReturn(expectedIPv6Packets[15])
-                .thenReturn(expectedIPv6Packets[16])
-                .thenReturn(expectedIPv6Packets[17])
-                .thenReturn(expectedIPv6Packets[18])
-                .thenReturn(expectedIPv6Packets[19])
-                .thenReturn(expectedIPv6Packets[20])
-                .thenReturn(expectedIPv6Packets[21])
-                .thenReturn(expectedIPv6Packets[22])
-                .thenReturn(expectedIPv6Packets[23]);
+        when(mockDeps.getDatagramPacketsFromMdnsPacket(
+                any(), any(MdnsPacket.class), eq(IPV6_ADDRESS), anyBoolean()))
+                .thenReturn(List.of(expectedIPv6Packets[0]))
+                .thenReturn(List.of(expectedIPv6Packets[1]))
+                .thenReturn(List.of(expectedIPv6Packets[2]))
+                .thenReturn(List.of(expectedIPv6Packets[3]))
+                .thenReturn(List.of(expectedIPv6Packets[4]))
+                .thenReturn(List.of(expectedIPv6Packets[5]))
+                .thenReturn(List.of(expectedIPv6Packets[6]))
+                .thenReturn(List.of(expectedIPv6Packets[7]))
+                .thenReturn(List.of(expectedIPv6Packets[8]))
+                .thenReturn(List.of(expectedIPv6Packets[9]))
+                .thenReturn(List.of(expectedIPv6Packets[10]))
+                .thenReturn(List.of(expectedIPv6Packets[11]))
+                .thenReturn(List.of(expectedIPv6Packets[12]))
+                .thenReturn(List.of(expectedIPv6Packets[13]))
+                .thenReturn(List.of(expectedIPv6Packets[14]))
+                .thenReturn(List.of(expectedIPv6Packets[15]))
+                .thenReturn(List.of(expectedIPv6Packets[16]))
+                .thenReturn(List.of(expectedIPv6Packets[17]))
+                .thenReturn(List.of(expectedIPv6Packets[18]))
+                .thenReturn(List.of(expectedIPv6Packets[19]))
+                .thenReturn(List.of(expectedIPv6Packets[20]))
+                .thenReturn(List.of(expectedIPv6Packets[21]))
+                .thenReturn(List.of(expectedIPv6Packets[22]))
+                .thenReturn(List.of(expectedIPv6Packets[23]));
 
         thread = new HandlerThread("MdnsServiceTypeClientTests");
         thread.start();
@@ -242,22 +244,13 @@
             return true;
         }).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
 
-        client = makeMdnsServiceTypeClient(mockPacketWriter);
+        client = makeMdnsServiceTypeClient();
     }
 
-    private MdnsServiceTypeClient makeMdnsServiceTypeClient(
-            @Nullable MdnsPacketWriter packetWriter) {
+    private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
         return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
                 mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
-                serviceCache) {
-            @Override
-            MdnsPacketWriter createMdnsPacketWriter() {
-                if (packetWriter == null) {
-                    return super.createMdnsPacketWriter();
-                }
-                return packetWriter;
-            }
-        };
+                serviceCache, featureFlags);
     }
 
     @After
@@ -569,10 +562,7 @@
         //MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
-        QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getQueryMode(),
-                false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
-                socketKey);
+        QueryTaskConfig config = new QueryTaskConfig(searchOptions.getQueryMode());
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
@@ -581,14 +571,14 @@
         // For the rest of queries in this burst, we will NOT ask for unicast response.
         for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
             int oldTransactionId = config.transactionId;
-            config = config.getConfigForNextRun();
+            config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
             assertFalse(config.expectUnicastResponse);
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
         // This is the first query of a new burst. We will ask for unicast response.
         int oldTransactionId = config.transactionId;
-        config = config.getConfigForNextRun();
+        config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
         assertTrue(config.expectUnicastResponse);
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
@@ -597,10 +587,7 @@
     public void testQueryTaskConfig_askForUnicastInFirstQuery() {
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
-        QueryTaskConfig config = new QueryTaskConfig(
-                searchOptions.getQueryMode(),
-                false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
-                socketKey);
+        QueryTaskConfig config = new QueryTaskConfig(searchOptions.getQueryMode());
 
         // This is the first query. We will ask for unicast response.
         assertTrue(config.expectUnicastResponse);
@@ -609,14 +596,14 @@
         // For the rest of queries in this burst, we will NOT ask for unicast response.
         for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
             int oldTransactionId = config.transactionId;
-            config = config.getConfigForNextRun();
+            config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
             assertFalse(config.expectUnicastResponse);
             assertEquals(config.transactionId, oldTransactionId + 1);
         }
 
         // This is the first query of a new burst. We will NOT ask for unicast response.
         int oldTransactionId = config.transactionId;
-        config = config.getConfigForNextRun();
+        config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
         assertFalse(config.expectUnicastResponse);
         assertEquals(config.transactionId, oldTransactionId + 1);
     }
@@ -697,27 +684,27 @@
 
     @Test
     public void testCombinedSubtypesQueriedWithMultipleListeners() throws Exception {
-        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
         final MdnsSearchOptions searchOptions1 = MdnsSearchOptions.newBuilder()
                 .addSubtype("subtype1").build();
         final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder()
                 .addSubtype("subtype2").build();
+        doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+                any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
         startSendAndReceive(mockListenerOne, searchOptions1);
-        currentThreadExecutor.getAndClearSubmittedRunnable().run();
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
 
         InOrder inOrder = inOrder(mockListenerOne, mockSocketClient, mockDeps);
 
         // Verify the query asks for subtype1
-        final ArgumentCaptor<DatagramPacket> subtype1QueryCaptor =
-                ArgumentCaptor.forClass(DatagramPacket.class);
-        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        final ArgumentCaptor<List<DatagramPacket>> subtype1QueryCaptor =
+                ArgumentCaptor.forClass(List.class);
         // Send twice for IPv4 and IPv6
         inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
                 subtype1QueryCaptor.capture(),
                 eq(socketKey), eq(false));
 
         final MdnsPacket subtype1Query = MdnsPacket.parse(
-                new MdnsPacketReader(subtype1QueryCaptor.getValue()));
+                new MdnsPacketReader(subtype1QueryCaptor.getValue().get(0)));
 
         assertEquals(2, subtype1Query.questions.size());
         assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
@@ -729,8 +716,8 @@
         inOrder.verify(mockDeps).removeMessages(any(), eq(EVENT_START_QUERYTASK));
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
 
-        final ArgumentCaptor<DatagramPacket> combinedSubtypesQueryCaptor =
-                ArgumentCaptor.forClass(DatagramPacket.class);
+        final ArgumentCaptor<List<DatagramPacket>> combinedSubtypesQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
         inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
                 combinedSubtypesQueryCaptor.capture(),
                 eq(socketKey), eq(false));
@@ -738,7 +725,7 @@
         inOrder.verify(mockDeps).sendMessageDelayed(any(), any(), anyLong());
 
         final MdnsPacket combinedSubtypesQuery = MdnsPacket.parse(
-                new MdnsPacketReader(combinedSubtypesQueryCaptor.getValue()));
+                new MdnsPacketReader(combinedSubtypesQueryCaptor.getValue().get(0)));
 
         assertEquals(3, combinedSubtypesQuery.questions.size());
         assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
@@ -754,15 +741,15 @@
         dispatchMessage();
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
 
-        final ArgumentCaptor<DatagramPacket> subtype2QueryCaptor =
-                ArgumentCaptor.forClass(DatagramPacket.class);
+        final ArgumentCaptor<List<DatagramPacket>> subtype2QueryCaptor =
+                ArgumentCaptor.forClass(List.class);
         // Send twice for IPv4 and IPv6
         inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
                 subtype2QueryCaptor.capture(),
                 eq(socketKey), eq(false));
 
         final MdnsPacket subtype2Query = MdnsPacket.parse(
-                new MdnsPacketReader(subtype2QueryCaptor.getValue()));
+                new MdnsPacketReader(subtype2QueryCaptor.getValue().get(0)));
 
         assertEquals(2, subtype2Query.questions.size());
         assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
@@ -1022,7 +1009,6 @@
     public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
             throws Exception {
         final String serviceInstanceName = "service-instance-1";
-        client = makeMdnsServiceTypeClient(mockPacketWriter);
         MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
                 .setRemoveExpiredService(true)
                 .setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
@@ -1060,7 +1046,6 @@
     public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
             throws Exception {
         final String serviceInstanceName = "service-instance-1";
-        client = makeMdnsServiceTypeClient(mockPacketWriter);
         startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
@@ -1086,7 +1071,6 @@
             throws Exception {
         //MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
         final String serviceInstanceName = "service-instance-1";
-        client = makeMdnsServiceTypeClient(mockPacketWriter);
         startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
         Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
 
@@ -1201,8 +1185,6 @@
 
     @Test
     public void testProcessResponse_Resolve() throws Exception {
-        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
         final String instanceName = "service-instance";
         final String[] hostname = new String[] { "testhost "};
         final String ipV4Address = "192.0.2.0";
@@ -1213,14 +1195,17 @@
         final MdnsSearchOptions resolveOptions2 = MdnsSearchOptions.newBuilder()
                 .setResolveInstanceName(instanceName).build();
 
+        doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+                any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+
         startSendAndReceive(mockListenerOne, resolveOptions1);
         startSendAndReceive(mockListenerTwo, resolveOptions2);
         // No need to verify order for both listeners; and order is not guaranteed between them
         InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
 
         // Verify a query for SRV/TXT was sent, but no PTR query
-        final ArgumentCaptor<DatagramPacket> srvTxtQueryCaptor =
-                ArgumentCaptor.forClass(DatagramPacket.class);
+        final ArgumentCaptor<List<DatagramPacket>> srvTxtQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
         // Send twice for IPv4 and IPv6
         inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
@@ -1232,7 +1217,7 @@
         verify(mockListenerTwo).onDiscoveryQuerySent(any(), anyInt());
 
         final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
-                new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
+                new MdnsPacketReader(srvTxtQueryCaptor.getValue().get(0)));
 
         final String[] serviceName = getTestServiceName(instanceName);
         assertEquals(1, srvTxtQueryPacket.questions.size());
@@ -1264,8 +1249,8 @@
 
         // Expect a query for A/AAAA
         dispatchMessage();
-        final ArgumentCaptor<DatagramPacket> addressQueryCaptor =
-                ArgumentCaptor.forClass(DatagramPacket.class);
+        final ArgumentCaptor<List<DatagramPacket>> addressQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
         inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
                 addressQueryCaptor.capture(),
@@ -1275,7 +1260,7 @@
         verify(mockListenerTwo, times(2)).onDiscoveryQuerySent(any(), anyInt());
 
         final MdnsPacket addressQueryPacket = MdnsPacket.parse(
-                new MdnsPacketReader(addressQueryCaptor.getValue()));
+                new MdnsPacketReader(addressQueryCaptor.getValue().get(0)));
         assertEquals(2, addressQueryPacket.questions.size());
         assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_A, hostname));
         assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_AAAA, hostname));
@@ -1317,8 +1302,6 @@
 
     @Test
     public void testRenewTxtSrvInResolve() throws Exception {
-        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
         final String instanceName = "service-instance";
         final String[] hostname = new String[] { "testhost "};
         final String ipV4Address = "192.0.2.0";
@@ -1327,12 +1310,15 @@
         final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
                 .setResolveInstanceName(instanceName).build();
 
+        doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+                any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+
         startSendAndReceive(mockListenerOne, resolveOptions);
         InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
 
         // Get the query for SRV/TXT
-        final ArgumentCaptor<DatagramPacket> srvTxtQueryCaptor =
-                ArgumentCaptor.forClass(DatagramPacket.class);
+        final ArgumentCaptor<List<DatagramPacket>> srvTxtQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
         // Send twice for IPv4 and IPv6
         inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
@@ -1342,7 +1328,7 @@
         assertNotNull(delayMessage);
 
         final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
-                new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
+                new MdnsPacketReader(srvTxtQueryCaptor.getValue().get(0)));
 
         final String[] serviceName = getTestServiceName(instanceName);
         assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_ANY, serviceName));
@@ -1386,8 +1372,8 @@
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
 
         // Expect a renewal query
-        final ArgumentCaptor<DatagramPacket> renewalQueryCaptor =
-                ArgumentCaptor.forClass(DatagramPacket.class);
+        final ArgumentCaptor<List<DatagramPacket>> renewalQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
         // Second and later sends are sent as "expect multicast response" queries
         inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
                 renewalQueryCaptor.capture(),
@@ -1396,7 +1382,7 @@
         assertNotNull(delayMessage);
         inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
         final MdnsPacket renewalPacket = MdnsPacket.parse(
-                new MdnsPacketReader(renewalQueryCaptor.getValue()));
+                new MdnsPacketReader(renewalQueryCaptor.getValue().get(0)));
         assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_ANY, serviceName));
         inOrder.verifyNoMoreInteractions();
 
@@ -1431,8 +1417,6 @@
 
     @Test
     public void testProcessResponse_ResolveExcludesOtherServices() {
-        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
         final String requestedInstance = "instance1";
         final String otherInstance = "instance2";
         final String ipV4Address = "192.0.2.0";
@@ -1499,8 +1483,6 @@
 
     @Test
     public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
-        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
         final String matchingInstance = "instance1";
         final String subtype = "_subtype";
         final String otherInstance = "instance2";
@@ -1587,8 +1569,6 @@
 
     @Test
     public void testProcessResponse_SubtypeChange() {
-        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
         final String matchingInstance = "instance1";
         final String subtype = "_subtype";
         final String ipV4Address = "192.0.2.0";
@@ -1670,8 +1650,6 @@
 
     @Test
     public void testNotifySocketDestroyed() throws Exception {
-        client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
         final String requestedInstance = "instance1";
         final String otherInstance = "instance2";
         final String ipV4Address = "192.0.2.0";
@@ -1946,6 +1924,138 @@
                 16 /* scheduledCount */);
     }
 
+    @Test
+    public void testSendQueryWithKnownAnswers() throws Exception {
+        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+                serviceCache,
+                MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
+
+        doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+                any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+
+        startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+        InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+        final ArgumentCaptor<List<DatagramPacket>> queryCaptor =
+                ArgumentCaptor.forClass(List.class);
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        // Send twice for IPv4 and IPv6
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+                queryCaptor.capture(), eq(socketKey), eq(false));
+        verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
+        assertNotNull(delayMessage);
+
+        final MdnsPacket queryPacket = MdnsPacket.parse(
+                new MdnsPacketReader(queryCaptor.getValue().get(0)));
+        assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR));
+
+        // Process a response
+        final String serviceName = "service-instance";
+        final String ipV4Address = "192.0.2.0";
+        final String[] subtypeLabels = Stream.concat(Stream.of("_subtype", "_sub"),
+                        Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+        final MdnsPacket packetWithoutSubtype = createResponse(
+                serviceName, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap() /* textAttributes */, TEST_TTL);
+        final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+                packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+        // Add a subtype PTR record
+        final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+        newAnswers.add(new MdnsPointerRecord(subtypeLabels, originalPtr.getReceiptTime(),
+                originalPtr.getCacheFlush(), originalPtr.getTtl(), originalPtr.getPointer()));
+        final MdnsPacket packetWithSubtype = new MdnsPacket(
+                packetWithoutSubtype.flags,
+                packetWithoutSubtype.questions,
+                newAnswers,
+                packetWithoutSubtype.authorityRecords,
+                packetWithoutSubtype.additionalRecords);
+        processResponse(packetWithSubtype, socketKey);
+
+        // Expect a query with known answers
+        dispatchMessage();
+        final ArgumentCaptor<List<DatagramPacket>> knownAnswersQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+                knownAnswersQueryCaptor.capture(), eq(socketKey), eq(false));
+
+        final MdnsPacket knownAnswersQueryPacket = MdnsPacket.parse(
+                new MdnsPacketReader(knownAnswersQueryCaptor.getValue().get(0)));
+        assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertFalse(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+    }
+
+    @Test
+    public void testSendQueryWithSubTypeWithKnownAnswers() throws Exception {
+        client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+                mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+                serviceCache,
+                MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
+
+        doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+                any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+
+        final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+                .addSubtype("subtype").build();
+        startSendAndReceive(mockListenerOne, options);
+        InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+        final ArgumentCaptor<List<DatagramPacket>> queryCaptor =
+                ArgumentCaptor.forClass(List.class);
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        // Send twice for IPv4 and IPv6
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+                queryCaptor.capture(), eq(socketKey), eq(false));
+        verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
+        assertNotNull(delayMessage);
+
+        final MdnsPacket queryPacket = MdnsPacket.parse(
+                new MdnsPacketReader(queryCaptor.getValue().get(0)));
+        final String[] subtypeLabels = Stream.concat(Stream.of("_subtype", "_sub"),
+                Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+        assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+
+        // Process a response
+        final String serviceName = "service-instance";
+        final String ipV4Address = "192.0.2.0";
+        final MdnsPacket packetWithoutSubtype = createResponse(
+                serviceName, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap() /* textAttributes */, TEST_TTL);
+        final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+                packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+        // Add a subtype PTR record
+        final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+        newAnswers.add(new MdnsPointerRecord(subtypeLabels, originalPtr.getReceiptTime(),
+                originalPtr.getCacheFlush(), originalPtr.getTtl(), originalPtr.getPointer()));
+        final MdnsPacket packetWithSubtype = new MdnsPacket(
+                packetWithoutSubtype.flags,
+                packetWithoutSubtype.questions,
+                newAnswers,
+                packetWithoutSubtype.authorityRecords,
+                packetWithoutSubtype.additionalRecords);
+        processResponse(packetWithSubtype, socketKey);
+
+        // Expect a query with known answers
+        dispatchMessage();
+        final ArgumentCaptor<List<DatagramPacket>> knownAnswersQueryCaptor =
+                ArgumentCaptor.forClass(List.class);
+        currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+        inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+                knownAnswersQueryCaptor.capture(), eq(socketKey), eq(false));
+
+        final MdnsPacket knownAnswersQueryPacket = MdnsPacket.parse(
+                new MdnsPacketReader(knownAnswersQueryCaptor.getValue().get(0)));
+        assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+        assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+        assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+    }
+
     private static MdnsServiceInfo matchServiceName(String name) {
         return argThat(info -> info.getServiceInstanceName().equals(name));
     }
@@ -1967,17 +2077,21 @@
         currentThreadExecutor.getAndClearLastScheduledRunnable().run();
         if (expectsUnicastResponse) {
             verify(mockSocketClient).sendPacketRequestingUnicastResponse(
-                    expectedIPv4Packets[index], socketKey, false);
+                    argThat(pkts -> pkts.get(0).equals(expectedIPv4Packets[index])),
+                    eq(socketKey), eq(false));
             if (multipleSocketDiscovery) {
                 verify(mockSocketClient).sendPacketRequestingUnicastResponse(
-                        expectedIPv6Packets[index], socketKey, false);
+                        argThat(pkts -> pkts.get(0).equals(expectedIPv6Packets[index])),
+                        eq(socketKey), eq(false));
             }
         } else {
             verify(mockSocketClient).sendPacketRequestingMulticastResponse(
-                    expectedIPv4Packets[index], socketKey, false);
+                    argThat(pkts -> pkts.get(0).equals(expectedIPv4Packets[index])),
+                    eq(socketKey), eq(false));
             if (multipleSocketDiscovery) {
                 verify(mockSocketClient).sendPacketRequestingMulticastResponse(
-                        expectedIPv6Packets[index], socketKey, false);
+                        argThat(pkts -> pkts.get(0).equals(expectedIPv6Packets[index])),
+                        eq(socketKey), eq(false));
             }
         }
         verify(mockDeps, times(index + 1))
@@ -2006,6 +2120,12 @@
                 && (name == null || Arrays.equals(q.name, name)));
     }
 
+    private static boolean hasAnswer(MdnsPacket packet, int type, @NonNull String[] name) {
+        return packet.answers.stream().anyMatch(q -> {
+            return q.getType() == type && (Arrays.equals(q.name, name));
+        });
+    }
+
     // A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
     // time.
     private class FakeExecutor extends ScheduledThreadPoolExecutor {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
index 8b7ab71..ab70e38 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertFalse;
@@ -26,17 +27,23 @@
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.Manifest.permission;
 import android.annotation.RequiresPermission;
 import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.InetAddresses;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.MulticastLock;
 import android.text.format.DateUtils;
+import android.util.Log;
 
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.SharedLog;
@@ -48,7 +55,9 @@
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.invocation.InvocationOnMock;
@@ -56,6 +65,8 @@
 import java.io.IOException;
 import java.net.DatagramPacket;
 import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -71,6 +82,7 @@
 
     @Mock private Context mContext;
     @Mock private WifiManager mockWifiManager;
+    @Mock private ConnectivityManager mockConnectivityManager;
     @Mock private MdnsSocket mockMulticastSocket;
     @Mock private MdnsSocket mockUnicastSocket;
     @Mock private MulticastLock mockMulticastLock;
@@ -84,6 +96,9 @@
     public void setup() throws RuntimeException, IOException {
         MockitoAnnotations.initMocks(this);
 
+        doReturn(mockConnectivityManager).when(mContext).getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+
         when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
                 .thenReturn(mockMulticastLock);
 
@@ -226,7 +241,7 @@
 
         // Sends a packet.
         DatagramPacket packet = getTestDatagramPacket();
-        mdnsClient.sendPacketRequestingMulticastResponse(packet,
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
         // it may not be called yet. So timeout is added.
@@ -234,7 +249,7 @@
         verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
 
         // Verify the packet is sent by the unicast socket.
-        mdnsClient.sendPacketRequestingUnicastResponse(packet,
+        mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
         verify(mockUnicastSocket, timeout(TIMEOUT).times(1)).send(packet);
@@ -279,7 +294,7 @@
 
         // Sends a packet.
         DatagramPacket packet = getTestDatagramPacket();
-        mdnsClient.sendPacketRequestingMulticastResponse(packet,
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
         // it may not be called yet. So timeout is added.
@@ -287,7 +302,7 @@
         verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
 
         // Verify the packet is sent by the multicast socket as well.
-        mdnsClient.sendPacketRequestingUnicastResponse(packet,
+        mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         verify(mockMulticastSocket, timeout(TIMEOUT).times(2)).send(packet);
         verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
@@ -320,19 +335,25 @@
 
     @Test
     public void testStartStop() throws IOException {
-        for (int i = 0; i < 5; i++) {
+        for (int i = 1; i <= 5; i++) {
             mdnsClient.startDiscovery();
 
             Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
             Thread socketThread = mdnsClient.sendThread;
+            final ArgumentCaptor<ConnectivityManager.NetworkCallback> cbCaptor =
+                    ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
 
             assertTrue(multicastReceiverThread.isAlive());
             assertTrue(socketThread.isAlive());
+            verify(mockConnectivityManager, times(i))
+                    .registerNetworkCallback(any(), cbCaptor.capture());
 
             mdnsClient.stopDiscovery();
 
             assertFalse(multicastReceiverThread.isAlive());
             assertFalse(socketThread.isAlive());
+            verify(mockConnectivityManager, times(i))
+                    .unregisterNetworkCallback(cbCaptor.getValue());
         }
     }
 
@@ -340,7 +361,7 @@
     public void testStopDiscovery_queueIsCleared() throws IOException {
         mdnsClient.startDiscovery();
         mdnsClient.stopDiscovery();
-        mdnsClient.sendPacketRequestingMulticastResponse(getTestDatagramPacket(),
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
 
         synchronized (mdnsClient.multicastPacketQueue) {
@@ -352,7 +373,7 @@
     public void testSendPacket_afterDiscoveryStops() throws IOException {
         mdnsClient.startDiscovery();
         mdnsClient.stopDiscovery();
-        mdnsClient.sendPacketRequestingMulticastResponse(getTestDatagramPacket(),
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
 
         synchronized (mdnsClient.multicastPacketQueue) {
@@ -366,7 +387,7 @@
         //MdnsConfigsFlagsImpl.mdnsPacketQueueMaxSize.override(2L);
         mdnsClient.startDiscovery();
         for (int i = 0; i < 100; i++) {
-            mdnsClient.sendPacketRequestingMulticastResponse(getTestDatagramPacket(),
+            mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
                     false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         }
 
@@ -464,9 +485,9 @@
 
         mdnsClient.startDiscovery();
         DatagramPacket packet = getTestDatagramPacket();
-        mdnsClient.sendPacketRequestingUnicastResponse(packet,
+        mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
-        mdnsClient.sendPacketRequestingMulticastResponse(packet,
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
 
         // Wait for the timer to be triggered.
@@ -497,9 +518,9 @@
         assertFalse(mdnsClient.receivedUnicastResponse);
         assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
 
-        mdnsClient.sendPacketRequestingUnicastResponse(packet,
+        mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
-        mdnsClient.sendPacketRequestingMulticastResponse(packet,
+        mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
                 false /* onlyUseIpv6OnIpv6OnlyNetworks */);
         Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
 
@@ -556,6 +577,49 @@
                 .onResponseReceived(any(), argThat(key -> key.getInterfaceIndex() == -1));
     }
 
+    @Test
+    public void testSendPacketWithMultipleDatagramPacket() throws IOException {
+        mdnsClient.startDiscovery();
+        final List<DatagramPacket> packets = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+            packets.add(new DatagramPacket(new byte[10 + i] /* buff */, 0 /* offset */,
+                    10 + i /* length */, MdnsConstants.IPV4_SOCKET_ADDR));
+        }
+
+        // Sends packets.
+        mdnsClient.sendPacketRequestingMulticastResponse(packets,
+                false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+        InOrder inOrder = inOrder(mockMulticastSocket);
+        for (int i = 0; i < 10; i++) {
+            // mockMulticastSocket.send() will be called on another thread. If we verify it
+            // immediately, it may not be called yet. So timeout is added.
+            inOrder.verify(mockMulticastSocket, timeout(TIMEOUT)).send(packets.get(i));
+        }
+    }
+
+    @Test
+    public void testSendPacketWithMultiplePacketsWithDifferentAddresses() throws IOException {
+        mdnsClient.startDiscovery();
+        final byte[] buffer = new byte[10];
+        final DatagramPacket ipv4Packet = new DatagramPacket(buffer, 0 /* offset */, buffer.length,
+                InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+        final DatagramPacket ipv6Packet = new DatagramPacket(buffer, 0 /* offset */, buffer.length,
+                InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
+
+        // Send packets with IPv4 and IPv6 then verify wtf logs and sending has never been called.
+        // Override the default TerribleFailureHandler, as that handler might terminate the process
+        // (if we're on an eng build).
+        final AtomicBoolean hasFailed = new AtomicBoolean(false);
+        final Log.TerribleFailureHandler originalHandler =
+                Log.setWtfHandler((tag, what, system) -> hasFailed.set(true));
+        testAndCleanup(() -> {
+            mdnsClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet, ipv6Packet),
+                    false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+            assertTrue(hasFailed.get());
+            verify(mockMulticastSocket, never()).send(any());
+        }, () -> Log.setWtfHandler(originalHandler));
+    }
+
     private DatagramPacket getTestDatagramPacket() {
         return new DatagramPacket(buf, 0, 5,
                 new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), 5353 /* port */));
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index f705bcb..5c3ad22 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -16,15 +16,22 @@
 
 package com.android.server.connectivity.mdns.util
 
+import android.net.InetAddresses
 import android.os.Build
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsDnsLabelIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLabelsLowerCase
-import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase
+import com.android.server.connectivity.mdns.MdnsConstants
+import com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED
+import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsPacket
+import com.android.server.connectivity.mdns.MdnsPacketReader
+import com.android.server.connectivity.mdns.MdnsPointerRecord
+import com.android.server.connectivity.mdns.MdnsRecord
+import com.android.server.connectivity.mdns.util.MdnsUtils.createQueryDatagramPackets
 import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Assert.assertArrayEquals
+import java.net.DatagramPacket
+import kotlin.test.assertContentEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -34,47 +41,6 @@
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsUtilsTest {
-    @Test
-    fun testToDnsLowerCase() {
-        assertEquals("test", toDnsLowerCase("TEST"))
-        assertEquals("test", toDnsLowerCase("TeSt"))
-        assertEquals("test", toDnsLowerCase("test"))
-        assertEquals("tÉst", toDnsLowerCase("TÉST"))
-        assertEquals("ţést", toDnsLowerCase("ţést"))
-        // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
-        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
-        assertEquals("test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
-                toDnsLowerCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "))
-        // Also test some characters where the first surrogate is not \ud800
-        assertEquals("test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
-                toDnsLowerCase("Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"))
-    }
-
-    @Test
-    fun testToDnsLabelsLowerCase() {
-        assertArrayEquals(arrayOf("test", "tÉst", "ţést"),
-            toDnsLabelsLowerCase(arrayOf("TeSt", "TÉST", "ţést")))
-    }
-
-    @Test
-    fun testEqualsIgnoreDnsCase() {
-        assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
-        assertTrue(equalsIgnoreDnsCase("TEST", "test"))
-        assertTrue(equalsIgnoreDnsCase("test", "TeSt"))
-        assertTrue(equalsIgnoreDnsCase("Tést", "tést"))
-        assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
-        // Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
-        // Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
-        assertTrue(equalsIgnoreDnsCase("test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
-                "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "))
-        // Also test some characters where the first surrogate is not \ud800
-        assertTrue(equalsIgnoreDnsCase("test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
-                "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
-                        "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"))
-    }
 
     @Test
     fun testTruncateServiceName() {
@@ -83,23 +49,112 @@
     }
 
     @Test
-    fun testEqualsLabelIgnoreDnsCase() {
-        assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
-        assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
+    fun testTypeEqualsOrIsSubtype() {
+        assertTrue(MdnsUtils.typeEqualsOrIsSubtype(
+            arrayOf("_type", "_tcp", "local"),
+            arrayOf("_type", "_TCP", "local")
+        ))
+        assertTrue(MdnsUtils.typeEqualsOrIsSubtype(
+            arrayOf("_type", "_tcp", "local"),
+            arrayOf("a", "_SUB", "_type", "_TCP", "local")
+        ))
+        assertFalse(MdnsUtils.typeEqualsOrIsSubtype(
+            arrayOf("_sub", "_type", "_tcp", "local"),
+                arrayOf("_type", "_TCP", "local")
+        ))
+        assertFalse(MdnsUtils.typeEqualsOrIsSubtype(
+                arrayOf("a", "_other", "_type", "_tcp", "local"),
+                arrayOf("a", "_SUB", "_type", "_TCP", "local")
+        ))
     }
 
     @Test
-    fun testTypeEqualsOrIsSubtype() {
-        assertTrue(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_type", "_tcp", "local"),
-            arrayOf("_type", "_TCP", "local")))
-        assertTrue(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_type", "_tcp", "local"),
-            arrayOf("a", "_SUB", "_type", "_TCP", "local")))
-        assertFalse(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_sub", "_type", "_tcp", "local"),
-                arrayOf("_type", "_TCP", "local")))
-        assertFalse(MdnsUtils.typeEqualsOrIsSubtype(
-                arrayOf("a", "_other", "_type", "_tcp", "local"),
-                arrayOf("a", "_SUB", "_type", "_TCP", "local")))
+    fun testCreateQueryDatagramPackets() {
+        // Question data bytes:
+        // Name label(17)(duplicated labels) + PTR type(2) + cacheFlush(2) = 21
+        //
+        // Known answers data bytes:
+        // Name label(17)(duplicated labels) + PTR type(2) + cacheFlush(2) + receiptTimeMillis(4)
+        // + Data length(2) + Pointer data(18)(duplicated labels) = 45
+        val questions = mutableListOf<MdnsRecord>()
+        val knownAnswers = mutableListOf<MdnsRecord>()
+        for (i in 1..100) {
+            questions.add(MdnsPointerRecord(arrayOf("_testservice$i", "_tcp", "local"), false))
+            knownAnswers.add(MdnsPointerRecord(
+                    arrayOf("_testservice$i", "_tcp", "local"),
+                    0L,
+                    false,
+                    4_500_000L,
+                    arrayOf("MyTestService$i", "_testservice$i", "_tcp", "local")
+            ))
+        }
+        // MdnsPacket data bytes:
+        // Questions(21 * 100) + Answers(45 * 100) = 6600 -> at least 5 packets
+        val query = MdnsPacket(
+                MdnsConstants.FLAGS_QUERY,
+                questions as List<MdnsRecord>,
+                knownAnswers as List<MdnsRecord>,
+                emptyList(),
+                emptyList()
+        )
+        // Expect the oversize MdnsPacket to be separated into 5 DatagramPackets.
+        val bufferSize = 1500
+        val packets = createQueryDatagramPackets(
+                ByteArray(bufferSize),
+                query,
+                MdnsConstants.IPV4_SOCKET_ADDR
+        )
+        assertEquals(5, packets.size)
+        assertTrue(packets.all { packet -> packet.length < bufferSize })
+
+        val mdnsPacket = createMdnsPacketFromMultipleDatagramPackets(packets)
+        assertEquals(query.flags, mdnsPacket.flags)
+        assertContentEquals(query.questions, mdnsPacket.questions)
+        assertContentEquals(query.answers, mdnsPacket.answers)
+    }
+
+    private fun createMdnsPacketFromMultipleDatagramPackets(
+            packets: List<DatagramPacket>
+    ): MdnsPacket {
+        var flags = 0
+        val questions = mutableListOf<MdnsRecord>()
+        val answers = mutableListOf<MdnsRecord>()
+        for ((index, packet) in packets.withIndex()) {
+            val mdnsPacket = MdnsPacket.parse(MdnsPacketReader(packet))
+            if (index != packets.size - 1) {
+                assertTrue((mdnsPacket.flags and FLAG_TRUNCATED) == FLAG_TRUNCATED)
+            }
+            flags = mdnsPacket.flags
+            questions.addAll(mdnsPacket.questions)
+            answers.addAll(mdnsPacket.answers)
+        }
+        return MdnsPacket(flags, questions, answers, emptyList(), emptyList())
+    }
+
+    @Test
+    fun testCheckAllPacketsWithSameAddress() {
+        val buffer = ByteArray(10)
+        val v4Packet = DatagramPacket(buffer, buffer.size, IPV4_SOCKET_ADDR)
+        val otherV4Packet = DatagramPacket(
+            buffer,
+            buffer.size,
+            InetAddresses.parseNumericAddress("192.0.2.1"),
+            1234
+        )
+        val v6Packet = DatagramPacket(ByteArray(10), 10, IPV6_SOCKET_ADDR)
+        val otherV6Packet = DatagramPacket(
+            buffer,
+            buffer.size,
+            InetAddresses.parseNumericAddress("2001:db8::"),
+            1234
+        )
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf()))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v4Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, otherV4Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, v6Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, otherV6Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v6Packet)))
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt
new file mode 100644
index 0000000..360a298
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSActiveNetworkInfoTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.INetd.PERMISSION_INTERNET
+import android.net.INetd.PERMISSION_NONE
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkInfo.DetailedState.BLOCKED
+import android.net.NetworkInfo.DetailedState.CONNECTED
+import android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+
+private fun nc() = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_WIFI)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSActiveNetworkInfoTest : CSTest() {
+
+    fun doTestGetActiveNetworkInfo(
+            changeEnabled: Boolean,
+            permissions: Int,
+            expectBlocked: Boolean
+    ) {
+        deps.setChangeIdEnabled(changeEnabled, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
+        doReturn(permissions).`when`(bpfNetMaps).getNetPermForUid(anyInt())
+
+        val agent = Agent(nc = nc())
+        agent.connect()
+
+        val networkInfo = cm.activeNetworkInfo
+        assertNotNull(networkInfo)
+        if (expectBlocked) {
+            assertEquals(BLOCKED, networkInfo.detailedState)
+        } else {
+            assertEquals(CONNECTED, networkInfo.detailedState)
+        }
+        assertEquals(expectBlocked, cm.activeNetwork == null)
+        agent.disconnect()
+    }
+
+    @Test
+    fun testGetActiveNetworkInfo() {
+        doReturn(true).`when`(bpfNetMaps).isUidNetworkingBlocked(anyInt(), anyBoolean())
+        doTestGetActiveNetworkInfo(
+                changeEnabled = true,
+                permissions = PERMISSION_NONE,
+                expectBlocked = true
+        )
+        doTestGetActiveNetworkInfo(
+                changeEnabled = false,
+                permissions = PERMISSION_INTERNET,
+                expectBlocked = true
+        )
+        // Network access is considered not blocked if the compat change is disabled and an app
+        // does not have PERMISSION_INTERNET
+        doTestGetActiveNetworkInfo(
+                changeEnabled = false,
+                permissions = PERMISSION_NONE,
+                expectBlocked = false
+        )
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
new file mode 100644
index 0000000..985d403
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBlockedReasonsTest.kt
@@ -0,0 +1,448 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER
+import android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED
+import android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND
+import android.net.ConnectivityManager.BLOCKED_REASON_DOZE
+import android.net.ConnectivityManager.BLOCKED_REASON_NETWORK_RESTRICTED
+import android.net.ConnectivityManager.BLOCKED_REASON_NONE
+import android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND
+import android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE
+import android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER
+import android.net.ConnectivityManager.FIREWALL_RULE_ALLOW
+import android.net.ConnectivityManager.FIREWALL_RULE_DENY
+import android.net.ConnectivitySettingsManager
+import android.net.INetd.PERMISSION_NONE
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION
+import android.os.Build
+import android.os.Process
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatusInt
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mockito.doReturn
+
+private fun cellNc() = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_CELLULAR)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+private fun cellRequest() = NetworkRequest.Builder()
+        .addTransportType(TRANSPORT_CELLULAR)
+        .build()
+private fun wifiNc() = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_WIFI)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .addCapability(NET_CAPABILITY_NOT_METERED)
+        .build()
+private fun wifiRequest() = NetworkRequest.Builder()
+        .addTransportType(TRANSPORT_WIFI)
+        .build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSBlockedReasonsTest : CSTest() {
+
+    inner class DetailedBlockedStatusCallback : TestableNetworkCallback() {
+        override fun onBlockedStatusChanged(network: Network, blockedReasons: Int) {
+            history.add(BlockedStatusInt(network, blockedReasons))
+        }
+
+        fun expectBlockedStatusChanged(network: Network, blockedReasons: Int) {
+            expect<BlockedStatusInt>(network) { it.reason == blockedReasons }
+        }
+    }
+
+    @Test
+    fun testBlockedReasons_onAvailable() {
+        doReturn(BLOCKED_REASON_DOZE or BLOCKED_METERED_REASON_DATA_SAVER)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+
+        val cellAgent = Agent(nc = cellNc())
+        cellAgent.connect()
+        val wifiAgent = Agent(nc = wifiNc())
+        wifiAgent.connect()
+
+        val cellCb = DetailedBlockedStatusCallback()
+        val wifiCb = DetailedBlockedStatusCallback()
+        cm.requestNetwork(cellRequest(), cellCb)
+        cm.requestNetwork(wifiRequest(), wifiCb)
+
+        cellCb.expectAvailableCallbacks(
+                cellAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_DOZE or BLOCKED_METERED_REASON_DATA_SAVER
+        )
+        wifiCb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_DOZE
+        )
+
+        cellAgent.disconnect()
+        wifiAgent.disconnect()
+        cm.unregisterNetworkCallback(cellCb)
+        cm.unregisterNetworkCallback(wifiCb)
+    }
+
+    @Test
+    fun testBlockedReasons_dataSaverChanged() {
+        doReturn(BLOCKED_REASON_APP_BACKGROUND or BLOCKED_METERED_REASON_DATA_SAVER)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        doReturn(true).`when`(netd).bandwidthEnableDataSaver(anyBoolean())
+
+        val cellCb = DetailedBlockedStatusCallback()
+        val wifiCb = DetailedBlockedStatusCallback()
+        cm.requestNetwork(cellRequest(), cellCb)
+        cm.requestNetwork(wifiRequest(), wifiCb)
+
+        val cellAgent = Agent(nc = cellNc())
+        cellAgent.connect()
+        val wifiAgent = Agent(nc = wifiNc())
+        wifiAgent.connect()
+        cellCb.expectAvailableCallbacks(
+                cellAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_APP_BACKGROUND or BLOCKED_METERED_REASON_DATA_SAVER
+        )
+        wifiCb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_APP_BACKGROUND
+        )
+
+        // Disable data saver
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setDataSaverEnabled(false)
+        cellCb.expectBlockedStatusChanged(cellAgent.network, BLOCKED_REASON_APP_BACKGROUND)
+
+        // waitForIdle since stubbing bpfNetMaps while CS handler thread calls
+        // bpfNetMaps.getNetPermForUid throws exception.
+        // The expectBlockedStatusChanged just above guarantees that the onBlockedStatusChanged
+        // method on this callback was called, but it does not guarantee that ConnectivityService
+        // has finished processing all onBlockedStatusChanged callbacks for all requests.
+        waitForIdle()
+        // Enable data saver
+        doReturn(BLOCKED_REASON_APP_BACKGROUND or BLOCKED_METERED_REASON_DATA_SAVER)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setDataSaverEnabled(true)
+        cellCb.expectBlockedStatusChanged(
+                cellAgent.network,
+                BLOCKED_REASON_APP_BACKGROUND or BLOCKED_METERED_REASON_DATA_SAVER
+        )
+        // BlockedStatus does not change for the non-metered network
+        wifiCb.assertNoCallback()
+
+        cellAgent.disconnect()
+        wifiAgent.disconnect()
+        cm.unregisterNetworkCallback(cellCb)
+        cm.unregisterNetworkCallback(wifiCb)
+    }
+
+    @Test
+    fun testBlockedReasons_setUidFirewallRule() {
+        doReturn(BLOCKED_REASON_DOZE or BLOCKED_METERED_REASON_USER_RESTRICTED)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+
+        val cellCb = DetailedBlockedStatusCallback()
+        val wifiCb = DetailedBlockedStatusCallback()
+        cm.requestNetwork(cellRequest(), cellCb)
+        cm.requestNetwork(wifiRequest(), wifiCb)
+
+        val cellAgent = Agent(nc = cellNc())
+        cellAgent.connect()
+        val wifiAgent = Agent(nc = wifiNc())
+        wifiAgent.connect()
+        cellCb.expectAvailableCallbacks(
+                cellAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_DOZE or BLOCKED_METERED_REASON_USER_RESTRICTED
+        )
+        wifiCb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_DOZE
+        )
+
+        // waitForIdle since stubbing bpfNetMaps while CS handler thread calls
+        // bpfNetMaps.getNetPermForUid throws exception.
+        // The expectBlockedStatusChanged just above guarantees that the onBlockedStatusChanged
+        // method on this callback was called, but it does not guarantee that ConnectivityService
+        // has finished processing all onBlockedStatusChanged callbacks for all requests.
+        waitForIdle()
+        // Set RULE_ALLOW on metered deny chain
+        doReturn(BLOCKED_REASON_DOZE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setUidFirewallRule(
+                FIREWALL_CHAIN_METERED_DENY_USER,
+                Process.myUid(),
+                FIREWALL_RULE_ALLOW
+        )
+        cellCb.expectBlockedStatusChanged(
+                cellAgent.network,
+                BLOCKED_REASON_DOZE
+        )
+        // BlockedStatus does not change for the non-metered network
+        wifiCb.assertNoCallback()
+
+        // Set RULE_DENY on metered deny chain
+        doReturn(BLOCKED_REASON_DOZE or BLOCKED_METERED_REASON_USER_RESTRICTED)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setUidFirewallRule(
+                FIREWALL_CHAIN_METERED_DENY_USER,
+                Process.myUid(),
+                FIREWALL_RULE_DENY
+        )
+        cellCb.expectBlockedStatusChanged(
+                cellAgent.network,
+                BLOCKED_REASON_DOZE or BLOCKED_METERED_REASON_USER_RESTRICTED
+        )
+        // BlockedStatus does not change for the non-metered network
+        wifiCb.assertNoCallback()
+
+        cellAgent.disconnect()
+        wifiAgent.disconnect()
+        cm.unregisterNetworkCallback(cellCb)
+        cm.unregisterNetworkCallback(wifiCb)
+    }
+
+    @Test
+    fun testBlockedReasons_setFirewallChainEnabled() {
+        doReturn(BLOCKED_REASON_NONE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+
+        val wifiCb = DetailedBlockedStatusCallback()
+        cm.requestNetwork(wifiRequest(), wifiCb)
+        val wifiAgent = Agent(nc = wifiNc())
+        wifiAgent.connect()
+        wifiCb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_NONE
+        )
+
+        // Enable dozable firewall chain
+        doReturn(BLOCKED_REASON_DOZE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_DOZABLE, true)
+        wifiCb.expectBlockedStatusChanged(
+                wifiAgent.network,
+                BLOCKED_REASON_DOZE
+        )
+
+        // Disable dozable firewall chain
+        doReturn(BLOCKED_REASON_NONE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_DOZABLE, false)
+        wifiCb.expectBlockedStatusChanged(
+                wifiAgent.network,
+                BLOCKED_REASON_NONE
+        )
+
+        wifiAgent.disconnect()
+        cm.unregisterNetworkCallback(wifiCb)
+    }
+
+    @Test
+    fun testBlockedReasons_replaceFirewallChain() {
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+
+        val wifiCb = DetailedBlockedStatusCallback()
+        cm.requestNetwork(wifiRequest(), wifiCb)
+        val wifiAgent = Agent(nc = wifiNc())
+        wifiAgent.connect()
+        wifiCb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_APP_BACKGROUND
+        )
+
+        // Put uid on background firewall chain
+        doReturn(BLOCKED_REASON_NONE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, intArrayOf(Process.myUid()))
+        wifiCb.expectBlockedStatusChanged(
+                wifiAgent.network,
+                BLOCKED_REASON_NONE
+        )
+
+        // Remove uid from background firewall chain
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, intArrayOf())
+        wifiCb.expectBlockedStatusChanged(
+                wifiAgent.network,
+                BLOCKED_REASON_APP_BACKGROUND
+        )
+
+        wifiAgent.disconnect()
+        cm.unregisterNetworkCallback(wifiCb)
+    }
+
+    @Test
+    fun testBlockedReasons_perAppDefaultNetwork() {
+        doReturn(BLOCKED_METERED_REASON_USER_RESTRICTED)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+
+        val cellCb = DetailedBlockedStatusCallback()
+        val wifiCb = DetailedBlockedStatusCallback()
+        cm.requestNetwork(cellRequest(), cellCb)
+        cm.requestNetwork(wifiRequest(), wifiCb)
+
+        val cellAgent = Agent(nc = cellNc())
+        cellAgent.connect()
+        val wifiAgent = Agent(nc = wifiNc())
+        wifiAgent.connect()
+
+        val cb = DetailedBlockedStatusCallback()
+        cm.registerDefaultNetworkCallback(cb)
+        cb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_NONE
+        )
+
+        // CS must send correct blocked reasons after per app default network change
+        ConnectivitySettingsManager.setMobileDataPreferredUids(context, setOf(Process.myUid()))
+        service.updateMobileDataPreferredUids()
+        cb.expectAvailableCallbacks(
+                cellAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_METERED_REASON_USER_RESTRICTED
+        )
+
+        // Remove per app default network request
+        ConnectivitySettingsManager.setMobileDataPreferredUids(context, setOf())
+        service.updateMobileDataPreferredUids()
+        cb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = BLOCKED_REASON_NONE
+        )
+
+        cellAgent.disconnect()
+        wifiAgent.disconnect()
+        cm.unregisterNetworkCallback(cellCb)
+        cm.unregisterNetworkCallback(wifiCb)
+        cm.unregisterNetworkCallback(cb)
+    }
+
+    private fun doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission: Boolean) {
+        doReturn(PERMISSION_NONE).`when`(bpfNetMaps).getNetPermForUid(Process.myUid())
+
+        val wifiCb = DetailedBlockedStatusCallback()
+        cm.requestNetwork(wifiRequest(), wifiCb)
+        val wifiAgent = Agent(nc = wifiNc())
+        wifiAgent.connect()
+        val expectedBlockedReason = if (blockedByNoInternetPermission) {
+            BLOCKED_REASON_NETWORK_RESTRICTED
+        } else {
+            BLOCKED_REASON_NONE
+        }
+        wifiCb.expectAvailableCallbacks(
+                wifiAgent.network,
+                validated = false,
+                blockedReason = expectedBlockedReason
+        )
+
+        // Enable background firewall chain
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true)
+        if (blockedByNoInternetPermission) {
+            wifiCb.expectBlockedStatusChanged(
+                    wifiAgent.network,
+                    BLOCKED_REASON_NETWORK_RESTRICTED or BLOCKED_REASON_APP_BACKGROUND
+            )
+        }
+        // waitForIdle since stubbing bpfNetMaps while CS handler thread calls
+        // bpfNetMaps.getNetPermForUid throws exception.
+        // ConnectivityService might haven't finished checking blocked status for all requests.
+        waitForIdle()
+
+        // Disable background firewall chain
+        doReturn(BLOCKED_REASON_NONE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(Process.myUid())
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, false)
+        if (blockedByNoInternetPermission) {
+            wifiCb.expectBlockedStatusChanged(
+                    wifiAgent.network,
+                    BLOCKED_REASON_NETWORK_RESTRICTED
+            )
+        } else {
+            // No callback is expected since blocked reasons does not change from
+            // BLOCKED_REASON_NONE.
+            wifiCb.assertNoCallback()
+        }
+    }
+
+    @Test
+    fun testBlockedReasonsNoInternetPermission_changeDisabled() {
+        deps.setChangeIdEnabled(false, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
+        doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission = false)
+    }
+
+    @Test
+    fun testBlockedReasonsNoInternetPermission_changeEnabled() {
+        deps.setChangeIdEnabled(true, NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION)
+        doTestBlockedReasonsNoInternetPermission(blockedByNoInternetPermission = true)
+    }
+
+    private fun doTestEnforceMeteredApnPolicy(restricted: Boolean) {
+        doReturn(restricted).`when`(bpfNetMaps).isUidRestrictedOnMeteredNetworks(Process.myUid())
+
+        val cellAgent = Agent(nc = cellNc())
+        cellAgent.connect()
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(cellRequest(), cb)
+
+        if (restricted) {
+            waitForIdle()
+            cb.assertNoCallback()
+        } else {
+            cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        }
+    }
+
+    @Test
+    fun testEnforceMeteredApnPolicy_restricted() {
+        doTestEnforceMeteredApnPolicy(restricted = true)
+    }
+
+    @Test
+    fun testEnforceMeteredApnPolicy_notRestricted() {
+        doTestEnforceMeteredApnPolicy(restricted = false)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
index be2b29c..0bad60d 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
@@ -20,6 +20,7 @@
 import android.content.Intent
 import android.content.pm.PackageManager.PERMISSION_DENIED
 import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.net.CaptivePortal
 import android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN
 import android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL
 import android.net.IpPrefix
@@ -33,23 +34,23 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
-import android.net.NetworkStack
-import android.net.CaptivePortal
 import android.net.NetworkRequest
 import android.net.NetworkScore
 import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.NetworkStack
 import android.net.RouteInfo
 import android.os.Build
 import android.os.Bundle
 import androidx.test.filters.SmallTest
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.assertThrows
 import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertEquals
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import kotlin.test.assertEquals
 
 // This allows keeping all the networks connected without having to file individual requests
 // for them.
@@ -95,16 +96,22 @@
         captivePortalCallback.expectAvailableCallbacksUnvalidated(wifiAgent)
         val signInIntent = startCaptivePortalApp(wifiAgent)
         // Remove the granted permissions
-        context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
-                PERMISSION_DENIED)
+        context.setPermission(
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                PERMISSION_DENIED
+        )
         context.setPermission(NETWORK_STACK, PERMISSION_DENIED)
         val captivePortal: CaptivePortal? = signInIntent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL)
-        assertThrows(SecurityException::class.java, { captivePortal?.reevaluateNetwork() })
+        captivePortal?.reevaluateNetwork()
+        verify(wifiAgent.networkMonitor, never()).forceReevaluation(anyInt())
     }
 
     private fun createWifiAgent(): CSAgentWrapper {
-        return Agent(score = keepScore(), lp = lp(WIFI_IFACE),
-                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        return Agent(
+            score = keepScore(),
+            lp = lp(WIFI_IFACE),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET)
+        )
     }
 
     private fun startCaptivePortalApp(networkAgent: CSAgentWrapper): Intent {
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
new file mode 100644
index 0000000..a7083dc
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivityservice
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.CALLBACK_AVAILABLE
+import android.net.ConnectivityManager.CALLBACK_BLK_CHANGED
+import android.net.ConnectivityManager.CALLBACK_CAP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_IP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED
+import android.net.ConnectivityManager.CALLBACK_LOST
+import android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL
+import android.net.LinkAddress
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkRequest
+import android.os.Build
+import com.android.net.module.util.BitUtils.packBits
+import com.android.server.CSTest
+import com.android.server.ConnectivityService
+import com.android.server.defaultLp
+import com.android.server.defaultNc
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.tryTest
+import java.lang.reflect.Modifier
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.test.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.spy
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class CSDeclaredMethodsForCallbacksTest : CSTest() {
+    private val mockedCallbackFlags = AtomicInteger(DECLARED_METHODS_ALL)
+    private lateinit var wrappedService: ConnectivityService
+
+    private val instrumentedCm by lazy { ConnectivityManager(context, wrappedService) }
+
+    @Before
+    fun setUpWrappedService() {
+        // Mock the callback flags set by ConnectivityManager when calling ConnectivityService, to
+        // simulate methods not being overridden
+        wrappedService = spy(service)
+        doAnswer { inv ->
+            service.requestNetwork(
+                inv.getArgument(0),
+                inv.getArgument(1),
+                inv.getArgument(2),
+                inv.getArgument(3),
+                inv.getArgument(4),
+                inv.getArgument(5),
+                inv.getArgument(6),
+                inv.getArgument(7),
+                inv.getArgument(8),
+                inv.getArgument(9),
+                mockedCallbackFlags.get())
+        }.`when`(wrappedService).requestNetwork(
+            anyInt(),
+            any(),
+            anyInt(),
+            any(),
+            anyInt(),
+            any(),
+            anyInt(),
+            anyInt(),
+            any(),
+            any(),
+            anyInt()
+        )
+        doAnswer { inv ->
+            service.listenForNetwork(
+                inv.getArgument(0),
+                inv.getArgument(1),
+                inv.getArgument(2),
+                inv.getArgument(3),
+                inv.getArgument(4),
+                inv.getArgument(5),
+                mockedCallbackFlags.get()
+            )
+        }.`when`(wrappedService)
+            .listenForNetwork(any(), any(), any(), anyInt(), any(), any(), anyInt())
+    }
+
+    @Test
+    fun testCallbacksAreFiltered() {
+        val requestCb = TestableNetworkCallback()
+        val listenCb = TestableNetworkCallback()
+        mockedCallbackFlags.withFlags(CALLBACK_IP_CHANGED, CALLBACK_LOST) {
+            instrumentedCm.requestNetwork(NetworkRequest.Builder().build(), requestCb)
+        }
+        mockedCallbackFlags.withFlags(CALLBACK_CAP_CHANGED) {
+            instrumentedCm.registerNetworkCallback(NetworkRequest.Builder().build(), listenCb)
+        }
+
+        with(Agent()) {
+            connect()
+            sendLinkProperties(defaultLp().apply {
+                addLinkAddress(LinkAddress("fe80:db8::123/64"))
+            })
+            sendNetworkCapabilities(defaultNc().apply {
+                addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+            })
+            disconnect()
+        }
+        waitForIdle()
+
+        // Only callbacks for the corresponding flags are called
+        requestCb.expect<CallbackEntry.LinkPropertiesChanged>()
+        requestCb.expect<CallbackEntry.Lost>()
+        requestCb.assertNoCallback(timeoutMs = 0L)
+
+        listenCb.expect<CallbackEntry.CapabilitiesChanged>()
+        listenCb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testDeclaredMethodsFlagsToString() {
+        assertEquals("NONE", ConnectivityService.declaredMethodsFlagsToString(0))
+        assertEquals("ALL", ConnectivityService.declaredMethodsFlagsToString(0.inv()))
+        assertEquals("AVAIL|NC|LP|BLK|LOCALINF", ConnectivityService.declaredMethodsFlagsToString(
+            (1 shl CALLBACK_AVAILABLE) or
+            (1 shl CALLBACK_CAP_CHANGED) or
+            (1 shl CALLBACK_IP_CHANGED) or
+            (1 shl CALLBACK_BLK_CHANGED) or
+            (1 shl CALLBACK_LOCAL_NETWORK_INFO_CHANGED)
+        ))
+
+        // EXPIRE_LEGACY_REQUEST (=8) is only used in ConnectivityManager and not included.
+        // CALLBACK_TRANSITIVE_CALLS_ONLY (=0) is not a callback so not included either.
+        assertEquals(
+            "PRECHK|AVAIL|LOSING|LOST|UNAVAIL|NC|LP|SUSP|RESUME|BLK|LOCALINF|0x7fffe101",
+            ConnectivityService.declaredMethodsFlagsToString(0x7fff_ffff)
+        )
+        // The toString method and the assertion above need to be updated if constants are added
+        val constants = ConnectivityManager::class.java.declaredFields.filter {
+            Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
+                    it.name.startsWith("CALLBACK_")
+        }
+        assertEquals(12, constants.size)
+    }
+}
+
+private fun AtomicInteger.withFlags(vararg flags: Int, action: () -> Unit) {
+    tryTest {
+        set(packBits(flags).toInt())
+        action()
+    } cleanup {
+        set(DECLARED_METHODS_ALL)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDestroySocketTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSDestroySocketTest.kt
new file mode 100644
index 0000000..bc5be78
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDestroySocketTest.kt
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.app.ActivityManager.UidFrozenStateChangedCallback
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN
+import android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND
+import android.net.ConnectivityManager.BLOCKED_REASON_NONE
+import android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND
+import android.net.ConnectivityManager.FIREWALL_RULE_ALLOW
+import android.net.ConnectivityManager.FIREWALL_RULE_DENY
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.os.Build
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener
+import com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+private const val TIMESTAMP = 1234L
+private const val TEST_UID = 1234
+private const val TEST_UID2 = 5678
+private const val TEST_CELL_IFACE = "test_rmnet"
+
+private fun cellNc() = NetworkCapabilities.Builder()
+        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+private fun cellLp() = LinkProperties().also{
+    it.interfaceName = TEST_CELL_IFACE
+}
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSDestroySocketTest : CSTest() {
+    private fun getRegisteredNetdUnsolicitedEventListener(): BaseNetdUnsolicitedEventListener {
+        val captor = ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener::class.java)
+        verify(netd).registerUnsolicitedEventListener(captor.capture())
+        return captor.value
+    }
+
+    private fun getUidFrozenStateChangedCallback(): UidFrozenStateChangedCallback {
+        val captor = ArgumentCaptor.forClass(UidFrozenStateChangedCallback::class.java)
+        verify(activityManager).registerUidFrozenStateChangedCallback(any(), captor.capture())
+        return captor.value
+    }
+
+    private fun doTestBackgroundRestrictionDestroySockets(
+            restrictionWithIdleNetwork: Boolean,
+            expectDelay: Boolean
+    ) {
+        val netdEventListener = getRegisteredNetdUnsolicitedEventListener()
+        val inOrder = inOrder(destroySocketsWrapper)
+
+        val cellAgent = Agent(nc = cellNc(), lp = cellLp())
+        cellAgent.connect()
+        if (restrictionWithIdleNetwork) {
+            // Make cell default network idle
+            netdEventListener.onInterfaceClassActivityChanged(
+                    false, // isActive
+                    cellAgent.network.netId,
+                    TIMESTAMP,
+                    TEST_UID
+            )
+        }
+
+        // Set deny rule on background chain for TEST_UID
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID)
+        cm.setUidFirewallRule(
+                FIREWALL_CHAIN_BACKGROUND,
+                TEST_UID,
+                FIREWALL_RULE_DENY
+        )
+        waitForIdle()
+        if (expectDelay) {
+            inOrder.verify(destroySocketsWrapper, never())
+                    .destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+        } else {
+            inOrder.verify(destroySocketsWrapper)
+                    .destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+        }
+
+        netdEventListener.onInterfaceClassActivityChanged(
+                true, // isActive
+                cellAgent.network.netId,
+                TIMESTAMP,
+                TEST_UID
+        )
+        waitForIdle()
+        if (expectDelay) {
+            inOrder.verify(destroySocketsWrapper)
+                    .destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+        } else {
+            inOrder.verify(destroySocketsWrapper, never())
+                    .destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+        }
+
+        cellAgent.disconnect()
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(DELAY_DESTROY_SOCKETS, true)])
+    fun testBackgroundAppDestroySockets() {
+        doTestBackgroundRestrictionDestroySockets(
+                restrictionWithIdleNetwork = true,
+                expectDelay = true
+        )
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(DELAY_DESTROY_SOCKETS, true)])
+    fun testBackgroundAppDestroySockets_activeNetwork() {
+        doTestBackgroundRestrictionDestroySockets(
+                restrictionWithIdleNetwork = false,
+                expectDelay = false
+        )
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(DELAY_DESTROY_SOCKETS, false)])
+    fun testBackgroundAppDestroySockets_featureIsDisabled() {
+        doTestBackgroundRestrictionDestroySockets(
+                restrictionWithIdleNetwork = true,
+                expectDelay = false
+        )
+    }
+
+    @Test
+    fun testReplaceFirewallChain() {
+        val netdEventListener = getRegisteredNetdUnsolicitedEventListener()
+        val inOrder = inOrder(destroySocketsWrapper)
+
+        val cellAgent = Agent(nc = cellNc(), lp = cellLp())
+        cellAgent.connect()
+        // Make cell default network idle
+        netdEventListener.onInterfaceClassActivityChanged(
+                false, // isActive
+                cellAgent.network.netId,
+                TIMESTAMP,
+                TEST_UID
+        )
+
+        // Set allow rule on background chain for TEST_UID
+        doReturn(BLOCKED_REASON_NONE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID)
+        cm.setUidFirewallRule(
+                FIREWALL_CHAIN_BACKGROUND,
+                TEST_UID,
+                FIREWALL_RULE_ALLOW
+        )
+        // Set deny rule on background chain for TEST_UID
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID2)
+        cm.setUidFirewallRule(
+                FIREWALL_CHAIN_BACKGROUND,
+                TEST_UID2,
+                FIREWALL_RULE_DENY
+        )
+
+        // Put only TEST_UID2 on background chain (deny TEST_UID and allow TEST_UID2)
+        doReturn(setOf(TEST_UID))
+                .`when`(bpfNetMaps).getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_BACKGROUND)
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID)
+        doReturn(BLOCKED_REASON_NONE)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID2)
+        cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, intArrayOf(TEST_UID2))
+        waitForIdle()
+        inOrder.verify(destroySocketsWrapper, never())
+                .destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+
+        netdEventListener.onInterfaceClassActivityChanged(
+                true, // isActive
+                cellAgent.network.netId,
+                TIMESTAMP,
+                TEST_UID
+        )
+        waitForIdle()
+        inOrder.verify(destroySocketsWrapper)
+                .destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+
+        cellAgent.disconnect()
+    }
+
+    private fun doTestDestroySockets(
+            isFrozen: Boolean,
+            denyOnBackgroundChain: Boolean,
+            enableBackgroundChain: Boolean,
+            expectDestroySockets: Boolean
+    ) {
+        val netdEventListener = getRegisteredNetdUnsolicitedEventListener()
+        val frozenStateCallback = getUidFrozenStateChangedCallback()
+
+        // Make cell default network idle
+        val cellAgent = Agent(nc = cellNc(), lp = cellLp())
+        cellAgent.connect()
+        netdEventListener.onInterfaceClassActivityChanged(
+                false, // isActive
+                cellAgent.network.netId,
+                TIMESTAMP,
+                TEST_UID
+        )
+
+        // Set deny rule on background chain for TEST_UID
+        doReturn(BLOCKED_REASON_APP_BACKGROUND)
+                .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID)
+        cm.setUidFirewallRule(
+                FIREWALL_CHAIN_BACKGROUND,
+                TEST_UID,
+                FIREWALL_RULE_DENY
+        )
+
+        // Freeze TEST_UID
+        frozenStateCallback.onUidFrozenStateChanged(
+                intArrayOf(TEST_UID),
+                intArrayOf(UID_FROZEN_STATE_FROZEN)
+        )
+
+        if (!isFrozen) {
+            // Unfreeze TEST_UID
+            frozenStateCallback.onUidFrozenStateChanged(
+                    intArrayOf(TEST_UID),
+                    intArrayOf(UID_FROZEN_STATE_UNFROZEN)
+            )
+        }
+        if (!enableBackgroundChain) {
+            // Disable background chain
+            cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, false)
+        }
+        if (!denyOnBackgroundChain) {
+            // Set allow rule on background chain for TEST_UID
+            doReturn(BLOCKED_REASON_NONE)
+                    .`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID)
+            cm.setUidFirewallRule(
+                    FIREWALL_CHAIN_BACKGROUND,
+                    TEST_UID,
+                    FIREWALL_RULE_ALLOW
+            )
+        }
+        verify(destroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+
+        // Make cell network active
+        netdEventListener.onInterfaceClassActivityChanged(
+                true, // isActive
+                cellAgent.network.netId,
+                TIMESTAMP,
+                TEST_UID
+        )
+        waitForIdle()
+
+        if (expectDestroySockets) {
+            verify(destroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+        } else {
+            verify(destroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
+        }
+    }
+
+    @Test
+    fun testDestroySockets_backgroundDeny_frozen() {
+        doTestDestroySockets(
+                isFrozen = true,
+                denyOnBackgroundChain = true,
+                enableBackgroundChain = true,
+                expectDestroySockets = true
+        )
+    }
+
+    @Test
+    fun testDestroySockets_backgroundDeny_nonFrozen() {
+        doTestDestroySockets(
+                isFrozen = false,
+                denyOnBackgroundChain = true,
+                enableBackgroundChain = true,
+                expectDestroySockets = true
+        )
+    }
+
+    @Test
+    fun testDestroySockets_backgroundAllow_frozen() {
+        doTestDestroySockets(
+                isFrozen = true,
+                denyOnBackgroundChain = false,
+                enableBackgroundChain = true,
+                expectDestroySockets = true
+        )
+    }
+
+    @Test
+    fun testDestroySockets_backgroundAllow_nonFrozen() {
+        // If the app is neither frozen nor under background restriction, sockets are not
+        // destroyed
+        doTestDestroySockets(
+                isFrozen = false,
+                denyOnBackgroundChain = false,
+                enableBackgroundChain = true,
+                expectDestroySockets = false
+        )
+    }
+
+    @Test
+    fun testDestroySockets_backgroundChainDisabled_nonFrozen() {
+        // If the app is neither frozen nor under background restriction, sockets are not
+        // destroyed
+        doTestDestroySockets(
+                isFrozen = false,
+                denyOnBackgroundChain = true,
+                enableBackgroundChain = false,
+                expectDestroySockets = false
+        )
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt
new file mode 100644
index 0000000..83ccccd
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.BpfNetMapsConstants.METERED_ALLOW_CHAINS
+import android.net.BpfNetMapsConstants.METERED_DENY_CHAINS
+import android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND
+import android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW
+import android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER
+import android.net.ConnectivityManager.FIREWALL_RULE_ALLOW
+import android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT
+import android.net.ConnectivityManager.FIREWALL_RULE_DENY
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.server.connectivity.ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertThrows
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class CSFirewallChainTest : CSTest() {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    // Tests for setFirewallChainEnabled on FIREWALL_CHAIN_BACKGROUND
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, false)])
+    fun setFirewallChainEnabled_backgroundChainDisabled() {
+        verifySetFirewallChainEnabledOnBackgroundDoesNothing()
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun setFirewallChainEnabled_backgroundChainEnabled_afterU() {
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true)
+        verify(bpfNetMaps).setChildChain(FIREWALL_CHAIN_BACKGROUND, true)
+
+        clearInvocations(bpfNetMaps)
+
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, false)
+        verify(bpfNetMaps).setChildChain(FIREWALL_CHAIN_BACKGROUND, false)
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun setFirewallChainEnabled_backgroundChainEnabled_uptoU() {
+        verifySetFirewallChainEnabledOnBackgroundDoesNothing()
+    }
+
+    private fun verifySetFirewallChainEnabledOnBackgroundDoesNothing() {
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true)
+        verify(bpfNetMaps, never()).setChildChain(anyInt(), anyBoolean())
+
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, false)
+        verify(bpfNetMaps, never()).setChildChain(anyInt(), anyBoolean())
+    }
+
+    // Tests for replaceFirewallChain on FIREWALL_CHAIN_BACKGROUND
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, false)])
+    fun replaceFirewallChain_backgroundChainDisabled() {
+        verifyReplaceFirewallChainOnBackgroundDoesNothing()
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun replaceFirewallChain_backgroundChainEnabled_afterU() {
+        val uids = intArrayOf(53, 42, 79)
+        cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, uids)
+        verify(bpfNetMaps).replaceUidChain(FIREWALL_CHAIN_BACKGROUND, uids)
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun replaceFirewallChain_backgroundChainEnabled_uptoU() {
+        verifyReplaceFirewallChainOnBackgroundDoesNothing()
+    }
+
+    private fun verifyReplaceFirewallChainOnBackgroundDoesNothing() {
+        val uids = intArrayOf(53, 42, 79)
+        cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, uids)
+        verify(bpfNetMaps, never()).replaceUidChain(anyInt(), any(IntArray::class.java))
+    }
+
+    // Tests for setUidFirewallRule on FIREWALL_CHAIN_BACKGROUND
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, false)])
+    fun setUidFirewallRule_backgroundChainDisabled() {
+        verifySetUidFirewallRuleOnBackgroundDoesNothing()
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun setUidFirewallRule_backgroundChainEnabled_afterU() {
+        val uid = 2345
+
+        cm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DEFAULT)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DENY)
+
+        clearInvocations(bpfNetMaps)
+
+        cm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DENY)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DENY)
+
+        clearInvocations(bpfNetMaps)
+
+        cm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_ALLOW)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_ALLOW)
+    }
+
+    @Test
+    @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+    @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun setUidFirewallRule_backgroundChainEnabled_uptoU() {
+        verifySetUidFirewallRuleOnBackgroundDoesNothing()
+    }
+
+    private fun verifySetUidFirewallRuleOnBackgroundDoesNothing() {
+        val uid = 2345
+
+        listOf(FIREWALL_RULE_DEFAULT, FIREWALL_RULE_ALLOW, FIREWALL_RULE_DENY).forEach { rule ->
+            cm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, rule)
+            verify(bpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt())
+        }
+    }
+
+    @Test
+    fun testSetFirewallChainEnabled_meteredChain() {
+        (METERED_ALLOW_CHAINS + METERED_DENY_CHAINS).forEach {
+            assertThrows(UnsupportedOperationException::class.java) {
+                cm.setFirewallChainEnabled(it, true)
+            }
+            assertThrows(UnsupportedOperationException::class.java) {
+                cm.setFirewallChainEnabled(it, false)
+            }
+        }
+    }
+
+    @Test
+    fun testAddUidToMeteredNetworkAllowList() {
+        val uid = 1001
+        cm.addUidToMeteredNetworkAllowList(uid)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_ALLOW)
+    }
+
+    @Test
+    fun testRemoveUidFromMeteredNetworkAllowList() {
+        val uid = 1001
+        cm.removeUidFromMeteredNetworkAllowList(uid)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_DENY)
+    }
+
+    @Test
+    fun testAddUidToMeteredNetworkDenyList() {
+        val uid = 1001
+        cm.addUidToMeteredNetworkDenyList(uid)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_DENY)
+    }
+
+    @Test
+    fun testRemoveUidFromMeteredNetworkDenyList() {
+        val uid = 1001
+        cm.removeUidFromMeteredNetworkDenyList(uid)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_ALLOW)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
new file mode 100644
index 0000000..77b06b2
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -0,0 +1,349 @@
+/*
+ * 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.server
+
+import android.net.InetAddresses
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.VpnManager.TYPE_VPN_OEM
+import android.net.VpnManager.TYPE_VPN_SERVICE
+import android.net.VpnManager.TYPE_VPN_LEGACY
+import android.net.VpnTransportInfo
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.server.connectivity.ConnectivityFlags
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.InOrder
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val VPN_IFNAME = "tun10041"
+private const val VPN_IFNAME2 = "tun10042"
+private const val WIFI_IFNAME = "wlan0"
+private const val TIMEOUT_MS = 1_000L
+private const val LONG_TIMEOUT_MS = 5_000
+
+private fun vpnNc(vpnType: Int = TYPE_VPN_SERVICE) = NetworkCapabilities.Builder().apply {
+    addTransportType(TRANSPORT_VPN)
+    removeCapability(NET_CAPABILITY_NOT_VPN)
+    addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+    setTransportInfo(
+            VpnTransportInfo(
+                    vpnType,
+                    "MySession12345",
+                    false /* bypassable */,
+                    false /* longLivedTcpConnectionsExpensive */
+            )
+    )
+}.build()
+
+private fun wifiNc() = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_WIFI)
+        .addCapability(NET_CAPABILITY_INTERNET)
+        .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .build()
+
+private fun nr(transport: Int) = NetworkRequest.Builder()
+        .clearCapabilities()
+        .addTransportType(transport).apply {
+            if (transport != TRANSPORT_VPN) {
+                addCapability(NET_CAPABILITY_NOT_VPN)
+            }
+        }.build()
+
+private fun lp(iface: String, vararg linkAddresses: LinkAddress) = LinkProperties().apply {
+    interfaceName = iface
+    for (linkAddress in linkAddresses) {
+        addLinkAddress(linkAddress)
+    }
+}
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class CSIngressDiscardRuleTests : CSTest() {
+    private val IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8:1::1")
+    private val IPV6_LINK_ADDRESS = LinkAddress(IPV6_ADDRESS, 64)
+    private val IPV6_ADDRESS2 = InetAddresses.parseNumericAddress("2001:db8:1::2")
+    private val IPV6_LINK_ADDRESS2 = LinkAddress(IPV6_ADDRESS2, 64)
+    private val IPV6_ADDRESS3 = InetAddresses.parseNumericAddress("2001:db8:1::3")
+    private val IPV6_LINK_ADDRESS3 = LinkAddress(IPV6_ADDRESS3, 64)
+    private val LOCAL_IPV6_ADDRRESS = InetAddresses.parseNumericAddress("fe80::1234")
+    private val LOCAL_IPV6_LINK_ADDRRESS = LinkAddress(LOCAL_IPV6_ADDRRESS, 64)
+
+    fun verifyNoMoreIngressDiscardRuleChange(inorder: InOrder) {
+        inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+        inorder.verify(bpfNetMaps, never()).removeIngressDiscardRule(any())
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_UpdateVpnAddress() {
+        // non-VPN network whose address will be not duplicated with VPN address
+        val wifiNc = wifiNc()
+        val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS3)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+
+        val nr = nr(TRANSPORT_VPN)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(nr, cb)
+        val nc = vpnNc()
+        val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val agent = Agent(nc = nc, lp = lp)
+        agent.connect()
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+
+        // IngressDiscardRule is added to the VPN address
+        verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+        verify(bpfNetMaps, never()).setIngressDiscardRule(LOCAL_IPV6_ADDRRESS, VPN_IFNAME)
+
+        // The VPN address is changed
+        val newLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+        agent.sendLinkProperties(newLp)
+        cb.expect<LinkPropertiesChanged>(agent.network)
+
+        // IngressDiscardRule is removed from the old VPN address and added to the new VPN address
+        verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+        verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS2, VPN_IFNAME)
+        verify(bpfNetMaps, never()).setIngressDiscardRule(LOCAL_IPV6_ADDRRESS, VPN_IFNAME)
+
+        agent.disconnect()
+        verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS2)
+
+        cm.unregisterNetworkCallback(cb)
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_UpdateInterfaceName() {
+        val inorder = inOrder(bpfNetMaps)
+
+        val nr = nr(TRANSPORT_VPN)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(nr, cb)
+        val nc = vpnNc()
+        val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val agent = Agent(nc = nc, lp = lp)
+        agent.connect()
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+
+        // IngressDiscardRule is added to the VPN address
+        inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+        verifyNoMoreIngressDiscardRuleChange(inorder)
+
+        // The VPN interface name is changed
+        val newlp = lp(VPN_IFNAME2, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        agent.sendLinkProperties(newlp)
+        cb.expect<LinkPropertiesChanged>(agent.network)
+
+        // IngressDiscardRule is updated with the new interface name
+        inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME2)
+        verifyNoMoreIngressDiscardRuleChange(inorder)
+
+        agent.disconnect()
+        inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS)
+
+        cm.unregisterNetworkCallback(cb)
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_DuplicatedIpAddress_UpdateVpnAddress() {
+        val inorder = inOrder(bpfNetMaps)
+
+        val wifiNc = wifiNc()
+        val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+
+        // IngressDiscardRule is not added to non-VPN interfaces
+        inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+
+        val nr = nr(TRANSPORT_VPN)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+        val vpnNc = vpnNc()
+        val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+        vpnAgent.connect()
+        cb.expectAvailableCallbacks(vpnAgent.network, validated = false)
+
+        // IngressDiscardRule is not added since the VPN address is duplicated with the Wi-Fi
+        // address
+        inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+
+        // The VPN address is changed to a different address from the Wi-Fi interface
+        val newVpnlp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+        vpnAgent.sendLinkProperties(newVpnlp)
+
+        // IngressDiscardRule is added to the VPN address since the VPN address is not duplicated
+        // with the Wi-Fi address
+        cb.expect<LinkPropertiesChanged>(vpnAgent.network)
+        inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS2, VPN_IFNAME)
+
+        // The VPN address is changed back to the same address as the Wi-Fi interface
+        vpnAgent.sendLinkProperties(vpnLp)
+        cb.expect<LinkPropertiesChanged>(vpnAgent.network)
+
+        // IngressDiscardRule for IPV6_ADDRESS2 is removed but IngressDiscardRule for
+        // IPV6_LINK_ADDRESS is not added since Wi-Fi also uses IPV6_LINK_ADDRESS
+        inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS2)
+        verifyNoMoreIngressDiscardRuleChange(inorder)
+
+        vpnAgent.disconnect()
+        verifyNoMoreIngressDiscardRuleChange(inorder)
+
+        cm.unregisterNetworkCallback(cb)
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_DuplicatedIpAddress_UpdateNonVpnAddress() {
+        val inorder = inOrder(bpfNetMaps)
+
+        val vpnNc = vpnNc()
+        val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+        vpnAgent.connect()
+
+        // IngressDiscardRule is added to the VPN address
+        inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+        verifyNoMoreIngressDiscardRuleChange(inorder)
+
+        val nr = nr(TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(nr, cb)
+        val wifiNc = wifiNc()
+        val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+        // IngressDiscardRule is removed since the VPN address is duplicated with the Wi-Fi address
+        inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+
+        // The Wi-Fi address is changed to a different address from the VPN interface
+        val newWifilp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+        wifiAgent.sendLinkProperties(newWifilp)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // IngressDiscardRule is added to the VPN address since the VPN address is not duplicated
+        // with the Wi-Fi address
+        inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+        verifyNoMoreIngressDiscardRuleChange(inorder)
+
+        // The Wi-Fi address is changed back to the same address as the VPN interface
+        wifiAgent.sendLinkProperties(wifiLp)
+        cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+        // IngressDiscardRule is removed since the VPN address is duplicated with the Wi-Fi address
+        inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+
+        // IngressDiscardRule is added to the VPN address since Wi-Fi is disconnected
+        wifiAgent.disconnect()
+        inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS))
+                .setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+
+        vpnAgent.disconnect()
+        inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS)
+
+        cm.unregisterNetworkCallback(cb)
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_UnregisterAfterReplacement() {
+        val wifiNc = wifiNc()
+        val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+        waitForIdle()
+
+        val vpnNc = vpnNc()
+        val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+        vpnAgent.connect()
+
+        // IngressDiscardRule is added since the Wi-Fi network is destroyed
+        verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+
+        // IngressDiscardRule is removed since the VPN network is destroyed
+        vpnAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+        waitForIdle()
+        verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+    }
+
+    @Test @FeatureFlags([Flag(ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING, false)])
+    fun testVpnIngressDiscardRule_FeatureDisabled() {
+        val nr = nr(TRANSPORT_VPN)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(nr, cb)
+        val nc = vpnNc()
+        val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val agent = Agent(nc = nc, lp = lp)
+        agent.connect()
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+
+        // IngressDiscardRule should not be added since feature is disabled
+        verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+    }
+
+    fun doTestVpnIngressDiscardRule_VpnType(vpnType: Int, expectAddRule: Boolean) {
+        val nr = nr(TRANSPORT_VPN)
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(nr, cb)
+        val nc = vpnNc(vpnType)
+        val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+        val agent = Agent(nc = nc, lp = lp)
+        agent.connect()
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+
+        if (expectAddRule) {
+            verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+        } else {
+            verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+        }
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_ServiceVpn() {
+        doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_SERVICE, expectAddRule = true)
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_LegacyVpn() {
+        // IngressDiscardRule should not be added to Legacy VPN
+        doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_LEGACY, expectAddRule = false)
+    }
+
+    @Test
+    fun testVpnIngressDiscardRule_OemVpn() {
+        // IngressDiscardRule should not be added to OEM VPN
+        doTestVpnIngressDiscardRule_VpnType(TYPE_VPN_OEM, expectAddRule = false)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index c1730a4..83fff87 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -38,6 +38,7 @@
 import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
 import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
 import android.net.RouteInfo
+import android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK
 import android.os.Build
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
@@ -47,12 +48,15 @@
 import kotlin.test.assertFailsWith
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
 
 private const val TIMEOUT_MS = 200L
 private const val MEDIUM_TIMEOUT_MS = 1_000L
@@ -88,10 +92,10 @@
 class CSLocalAgentTests : CSTest() {
     val multicastRoutingConfigMinScope =
                 MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, 4)
-                .build();
+                .build()
     val multicastRoutingConfigSelected =
                 MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_SELECTED)
-                .build();
+                .build()
     val upstreamSelectorAny = NetworkRequest.Builder()
                 .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                 .build()
@@ -205,6 +209,9 @@
                 nc = nc(TRANSPORT_THREAD, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp(name),
                 lnc = localNetworkConfig,
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
         )
         return localAgent
     }
@@ -219,9 +226,12 @@
                 nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET))
     }
 
-    private fun sendLocalNetworkConfig(localAgent: CSAgentWrapper,
-                upstreamSelector: NetworkRequest?, upstreamConfig: MulticastRoutingConfig,
-                downstreamConfig: MulticastRoutingConfig) {
+    private fun sendLocalNetworkConfig(
+            localAgent: CSAgentWrapper,
+            upstreamSelector: NetworkRequest?,
+            upstreamConfig: MulticastRoutingConfig,
+            downstreamConfig: MulticastRoutingConfig
+    ) {
         val newLnc = LocalNetworkConfig.Builder()
                 .setUpstreamSelector(upstreamSelector)
                 .setUpstreamMulticastRoutingConfig(upstreamConfig)
@@ -458,7 +468,6 @@
         wifiAgent.disconnect()
     }
 
-
     @Test
     fun testUnregisterUpstreamAfterReplacement_SameIfaceName() {
         doTestUnregisterUpstreamAfterReplacement(true)
@@ -824,4 +833,59 @@
 
         listenCb.expect<Lost>()
     }
+
+    fun doTestLocalNetworkRequest(
+            request: NetworkRequest,
+            enableMatchLocalNetwork: Boolean,
+            expectCallback: Boolean
+    ) {
+        deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(enableMatchLocalNetwork, ENABLE_MATCH_LOCAL_NETWORK)
+
+        val requestCb = TestableNetworkCallback()
+        val listenCb = TestableNetworkCallback()
+        cm.requestNetwork(request, requestCb)
+        cm.registerNetworkCallback(request, listenCb)
+
+        val localAgent = createLocalAgent("local0", FromS(LocalNetworkConfig.Builder().build()))
+        localAgent.connect()
+
+        if (expectCallback) {
+            requestCb.expectAvailableCallbacks(localAgent.network, validated = false)
+            listenCb.expectAvailableCallbacks(localAgent.network, validated = false)
+        } else {
+            waitForIdle()
+            requestCb.assertNoCallback(timeoutMs = 0)
+            listenCb.assertNoCallback(timeoutMs = 0)
+        }
+        localAgent.disconnect()
+    }
+
+    @Test
+    fun testLocalNetworkRequest() {
+        val request = NetworkRequest.Builder().build()
+        // If ENABLE_MATCH_LOCAL_NETWORK is false, request is not satisfied by local network
+        doTestLocalNetworkRequest(
+                request,
+                enableMatchLocalNetwork = false,
+                expectCallback = false)
+        // If ENABLE_MATCH_LOCAL_NETWORK is true, request is satisfied by local network
+        doTestLocalNetworkRequest(
+                request,
+                enableMatchLocalNetwork = true,
+                expectCallback = true)
+    }
+
+    @Test
+    fun testLocalNetworkRequest_withCapability() {
+        val request = NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build()
+        doTestLocalNetworkRequest(
+                request,
+                enableMatchLocalNetwork = false,
+                expectCallback = true)
+        doTestLocalNetworkRequest(
+                request,
+                enableMatchLocalNetwork = true,
+                expectCallback = true)
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
new file mode 100644
index 0000000..fc2a06c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSQueuedCallbacksTest.kt
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivityservice
+
+import android.app.ActivityManager.UidFrozenStateChangedCallback
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN
+import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN
+import android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER
+import android.net.ConnectivityManager.BLOCKED_REASON_NONE
+import android.net.ConnectivitySettingsManager
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkPolicyManager.NetworkPolicyCallback
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.server.CALLING_UID_UNMOCKED
+import com.android.server.CSAgentWrapper
+import com.android.server.CSTest
+import com.android.server.FromS
+import com.android.server.HANDLER_TIMEOUT_MS
+import com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS
+import com.android.server.defaultLp
+import com.android.server.defaultNc
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
+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.TestableNetworkCallback
+import com.android.testutils.visibleOnHandlerThread
+import com.android.testutils.waitForIdleSerialExecutor
+import java.util.Collections
+import kotlin.test.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+
+private const val TEST_UID = 42
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class CSQueuedCallbacksTest(freezingBehavior: FreezingBehavior) : CSTest() {
+    companion object {
+        enum class FreezingBehavior {
+            UID_FROZEN,
+            UID_NOT_FROZEN,
+            UID_FROZEN_FEATURE_DISABLED
+        }
+
+        // Use a parameterized test with / without freezing to make it easy to compare and make sure
+        // freezing behavior (which callbacks are sent in which order) stays close to what happens
+        // without freezing.
+        @JvmStatic
+        @Parameterized.Parameters(name = "freezingBehavior={0}")
+        fun freezingBehavior() = listOf(
+            FreezingBehavior.UID_FROZEN,
+            FreezingBehavior.UID_NOT_FROZEN,
+            FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+        )
+
+        private val TAG = CSQueuedCallbacksTest::class.simpleName
+            ?: fail("Could not get test class name")
+    }
+
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    private val mockedBlockedReasonsPerUid = Collections.synchronizedMap(mutableMapOf(
+        Process.myUid() to BLOCKED_REASON_NONE,
+        TEST_UID to BLOCKED_REASON_NONE
+    ))
+
+    private val freezeUids = freezingBehavior != FreezingBehavior.UID_NOT_FROZEN
+    private val expectAllCallbacks = freezingBehavior == FreezingBehavior.UID_NOT_FROZEN ||
+            freezingBehavior == FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+    init {
+        setFeatureEnabled(
+            QUEUE_CALLBACKS_FOR_FROZEN_APPS,
+            freezingBehavior != FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
+        )
+    }
+
+    @Before
+    fun subclassSetUp() {
+        // Ensure cellular stays up. CS is recreated for each test so no cleanup is necessary.
+//        cm.requestNetwork(
+//            NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(),
+//            TestableNetworkCallback()
+//        )
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_UpdatesAreReceived() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        val lpChangeOnConnect = agent.sendLpChange { setLinkAddresses("fe80:db8::123/64") }
+        val ncChangeOnConnect = agent.sendNcChange {
+            addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+        }
+
+        maybeSetUidsFrozen(true, TEST_UID)
+
+        val lpChange1WhileFrozen = agent.sendLpChange {
+            setLinkAddresses("fe80:db8::126/64")
+        }
+        val ncChange1WhileFrozen = agent.sendNcChange {
+            removeCapability(NET_CAPABILITY_NOT_ROAMING)
+        }
+        val ncChange2WhileFrozen = agent.sendNcChange {
+            addCapability(NET_CAPABILITY_NOT_ROAMING)
+            addCapability(NET_CAPABILITY_NOT_CONGESTED)
+        }
+        val lpChange2WhileFrozen = agent.sendLpChange {
+            setLinkAddresses("fe80:db8::125/64")
+        }
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        // Verify callbacks that are sent before freezing
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        cb.expectLpWith(agent, lpChangeOnConnect)
+        cb.expectNcWith(agent, ncChangeOnConnect)
+
+        // Below callbacks should be skipped if the processes were frozen, since a single callback
+        // will be sent with the latest state after unfreezing
+        if (expectAllCallbacks) {
+            cb.expectLpWith(agent, lpChange1WhileFrozen)
+            cb.expectNcWith(agent, ncChange1WhileFrozen)
+        }
+
+        cb.expectNcWith(agent, ncChange2WhileFrozen)
+        cb.expectLpWith(agent, lpChange2WhileFrozen)
+
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_SuspendedUnsuspendedWhileFrozen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+        val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectNcWith(agent, rmCap)
+            cb.expect<Suspended>(agent)
+            cb.expectNcWith(agent, addCap)
+            cb.expect<Resumed>(agent)
+        } else {
+            // When frozen, a single NetworkCapabilitiesChange will be sent at unfreezing time,
+            // with nc actually identical to the original ones. This is because NetworkCapabilities
+            // callbacks were sent, but CS does not keep initial NetworkCapabilities in memory, so
+            // it cannot detect A->B->A.
+            cb.expect<CapabilitiesChanged>(agent) {
+                it.caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+            }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_UnsuspendedWhileFrozen_GetResumedCallbackWhenUnfrozen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        cb.expectNcWith(agent, rmCap)
+        cb.expect<Suspended>(agent)
+        cb.expectNcWith(agent, addCap)
+        cb.expect<Resumed>(agent)
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_BlockedUnblockedWhileFrozen_SingleCallbackIfFrozen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+
+        maybeSetUidsFrozen(true, TEST_UID)
+        setUidsBlockedForDataSaver(true, TEST_UID)
+        setUidsBlockedForDataSaver(false, TEST_UID)
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expect<BlockedStatus>(agent) { it.blocked }
+        }
+        // The unblocked callback is sent in any case (with the latest blocked reason), as the
+        // blocked reason may have changed, and ConnectivityService cannot know that it is the same
+        // as the original reason as it does not keep pre-freeze blocked reasons in memory.
+        cb.expect<BlockedStatus>(agent) { !it.blocked }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testFrozenWhileNetworkConnects_BlockedWhileFrozen_GetLastBlockedCallbackOnlyIfFrozen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+
+        maybeSetUidsFrozen(true, TEST_UID)
+        setUidsBlockedForDataSaver(true, TEST_UID)
+        setUidsBlockedForDataSaver(false, TEST_UID)
+        setUidsBlockedForDataSaver(true, TEST_UID)
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(agent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expect<BlockedStatus>(agent) { it.blocked }
+            cb.expect<BlockedStatus>(agent) { !it.blocked }
+        }
+        cb.expect<BlockedStatus>(agent) { it.blocked }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testNetworkCallback_NetworkToggledWhileFrozen_NotSeen() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+        wifiAgent.disconnect()
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+            cb.expect<Lost>(wifiAgent)
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testNetworkCallback_NetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
+        }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        waitForIdle()
+        agent.makeValidationSuccess()
+        val lpChange = agent.sendLpChange {
+            setLinkAddresses("fe80:db8::123/64")
+        }
+        val suspendedChange = agent.sendNcChange {
+            removeCapability(NET_CAPABILITY_NOT_SUSPENDED)
+        }
+        setUidsBlockedForDataSaver(true, TEST_UID)
+
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        val expectLatestStatusInOnAvailable = !expectAllCallbacks
+        cb.expectAvailableCallbacks(
+            agent.network,
+            suspended = expectLatestStatusInOnAvailable,
+            validated = expectLatestStatusInOnAvailable,
+            blocked = expectLatestStatusInOnAvailable
+        )
+        if (expectAllCallbacks) {
+            cb.expectNcWith(agent) { addCapability(NET_CAPABILITY_VALIDATED) }
+            cb.expectLpWith(agent, lpChange)
+            cb.expectNcWith(agent, suspendedChange)
+            cb.expect<Suspended>(agent)
+            cb.expect<BlockedStatus>(agent) { it.blocked }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testNetworkCallback_LocalNetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerNetworkCallback(
+                NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build(),
+                cb
+            )
+        }
+        val upstreamAgent = Agent(
+            nc = defaultNc()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET),
+            lp = defaultLp().apply { interfaceName = "wlan0" }
+        ).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+
+        val lnc = LocalNetworkConfig.Builder().build()
+        val localAgent = Agent(
+            nc = defaultNc()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .removeCapability(NET_CAPABILITY_INTERNET),
+            lp = defaultLp().apply { interfaceName = "local42" },
+            lnc = FromS(lnc)
+        ).apply { connect() }
+        localAgent.sendLocalNetworkConfig(
+            LocalNetworkConfig.Builder()
+                .setUpstreamSelector(
+                    NetworkRequest.Builder()
+                        .addCapability(NET_CAPABILITY_INTERNET)
+                        .build()
+                )
+                .build()
+        )
+
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(
+            localAgent.network,
+            validated = false,
+            upstream = if (expectAllCallbacks) null else upstreamAgent.network
+        )
+        if (expectAllCallbacks) {
+            cb.expect<LocalInfoChanged>(localAgent) {
+                it.info.upstreamNetwork == upstreamAgent.network
+            }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testNetworkRequest_NetworkSwitchesWhileFrozen_ReceiveLastNetworkUpdatesOnly() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.requestNetwork(NetworkRequest.Builder().build(), cb)
+        }
+        val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+        val ethAgent = Agent(TRANSPORT_ETHERNET).apply { connect() }
+        waitForIdle()
+        ethAgent.makeValidationSuccess()
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+            cb.expectAvailableCallbacks(ethAgent.network, validated = false)
+            cb.expectNcWith(ethAgent) { addCapability(NET_CAPABILITY_VALIDATED) }
+        } else {
+            cb.expectAvailableCallbacks(ethAgent.network, validated = true)
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testNetworkRequest_NetworkSwitchesBackWhileFrozen_ReceiveNoAvailableCallback() {
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.requestNetwork(NetworkRequest.Builder().build(), cb)
+        }
+        val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
+        maybeSetUidsFrozen(true, TEST_UID)
+        val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
+        waitForIdle()
+
+        // CS switches back to validated cell over non-validated Wi-Fi
+        cellAgent.makeValidationSuccess()
+        val cellLpChange = cellAgent.sendLpChange {
+            setLinkAddresses("fe80:db8::123/64")
+        }
+        setUidsBlockedForDataSaver(true, TEST_UID)
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+            // There is an extra "double validated" CapabilitiesChange callback (b/245893397), so
+            // callbacks are (AVAIL, NC, LP), extra NC, then further updates (LP and BLK here).
+            cb.expectAvailableDoubleValidatedCallbacks(cellAgent.network)
+            cb.expectLpWith(cellAgent, cellLpChange)
+            cb.expect<BlockedStatus>(cellAgent) { it.blocked }
+        } else {
+            cb.expectNcWith(cellAgent) {
+                addCapability(NET_CAPABILITY_VALIDATED)
+            }
+            cb.expectLpWith(cellAgent, cellLpChange)
+            cb.expect<BlockedStatus>(cellAgent) { it.blocked }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultRequestFiled_ReceiveChangeCallbacks() {
+        val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
+        waitForIdle()
+
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerDefaultNetworkCallback(cb)
+        }
+        maybeSetUidsFrozen(true, TEST_UID)
+
+        // Change LinkProperties twice before the per-app network request is applied
+        val lpChange1 = cellAgent.sendLpChange {
+            setLinkAddresses("fe80:db8::123/64")
+        }
+        val lpChange2 = cellAgent.sendLpChange {
+            setLinkAddresses("fe80:db8::124/64")
+        }
+        setMobileDataPreferredUids(setOf(TEST_UID))
+
+        // Change NetworkCapabilities after the per-app network request is applied
+        val ncChange = cellAgent.sendNcChange {
+            addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+        }
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        // Even if a per-app network request was filed to replace the default network request for
+        // the app, all network change callbacks are received
+        cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+        if (expectAllCallbacks) {
+            cb.expectLpWith(cellAgent, lpChange1)
+        }
+        cb.expectLpWith(cellAgent, lpChange2)
+        cb.expectNcWith(cellAgent, ncChange)
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    @Test
+    fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultToggled_GetStatusUpdateCallbacksOnly() {
+        // Add validated Wi-Fi and non-validated cell, expect Wi-Fi is preferred by default
+        val wifiAgent = Agent(TRANSPORT_WIFI, baseNc = makeInternetNc()).apply { connect() }
+        wifiAgent.makeValidationSuccess()
+        val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
+        waitForIdle()
+
+        val cb = TestableNetworkCallback(logTag = TAG)
+        withCallingUid(TEST_UID) {
+            cm.registerDefaultNetworkCallback(cb)
+        }
+        maybeSetUidsFrozen(true, TEST_UID)
+
+        // LP change on the original Wi-Fi network
+        val lpChange = wifiAgent.sendLpChange {
+            setLinkAddresses("fe80:db8::123/64")
+        }
+        // Set per-app default to cell, then unset it
+        setMobileDataPreferredUids(setOf(TEST_UID))
+        setMobileDataPreferredUids(emptySet())
+
+        maybeSetUidsFrozen(false, TEST_UID)
+
+        cb.expectAvailableCallbacks(wifiAgent.network)
+        if (expectAllCallbacks) {
+            cb.expectLpWith(wifiAgent, lpChange)
+            cb.expectAvailableCallbacks(cellAgent.network, validated = false)
+            // Cellular stops being foreground since it is now matched for this app
+            cb.expect<CapabilitiesChanged> { it.caps.hasCapability(NET_CAPABILITY_FOREGROUND) }
+            cb.expectAvailableCallbacks(wifiAgent.network)
+        } else {
+            // After switching to cell and back while frozen, only network attribute update
+            // callbacks (and not AVAILABLE) for the original Wi-Fi network should be sent
+            cb.expect<CapabilitiesChanged>(wifiAgent)
+            cb.expectLpWith(wifiAgent, lpChange)
+            cb.expect<BlockedStatus> { !it.blocked }
+        }
+        cb.assertNoCallback(timeoutMs = 0L)
+    }
+
+    private fun setUidsBlockedForDataSaver(blocked: Boolean, vararg uid: Int) {
+        val reason = if (blocked) {
+            BLOCKED_METERED_REASON_DATA_SAVER
+        } else {
+            BLOCKED_REASON_NONE
+        }
+        if (deps.isAtLeastV) {
+            visibleOnHandlerThread(csHandler) {
+                service.handleBlockedReasonsChanged(uid.map { android.util.Pair(it, reason) })
+            }
+        } else {
+            notifyLegacyBlockedReasonChanged(reason, uid)
+            waitForIdle()
+        }
+    }
+
+    @Suppress("DEPRECATION")
+    private fun notifyLegacyBlockedReasonChanged(reason: Int, uids: IntArray) {
+        val cbCaptor = ArgumentCaptor.forClass(NetworkPolicyCallback::class.java)
+        verify(context.networkPolicyManager).registerNetworkPolicyCallback(
+            any(),
+            cbCaptor.capture()
+        )
+        uids.forEach {
+            cbCaptor.value.onUidBlockedReasonChanged(it, reason)
+        }
+    }
+
+    private fun withCallingUid(uid: Int, action: () -> Unit) {
+        deps.callingUid = uid
+        action()
+        deps.callingUid = CALLING_UID_UNMOCKED
+    }
+
+    private fun getUidFrozenStateChangedCallback(): UidFrozenStateChangedCallback {
+        val captor = ArgumentCaptor.forClass(UidFrozenStateChangedCallback::class.java)
+        verify(activityManager).registerUidFrozenStateChangedCallback(any(), captor.capture())
+        return captor.value
+    }
+
+    private fun maybeSetUidsFrozen(frozen: Boolean, vararg uids: Int) {
+        if (!freezeUids) return
+        val state = if (frozen) UID_FROZEN_STATE_FROZEN else UID_FROZEN_STATE_UNFROZEN
+        getUidFrozenStateChangedCallback()
+            .onUidFrozenStateChanged(uids, IntArray(uids.size) { state })
+        waitForIdle()
+    }
+
+    private fun CSAgentWrapper.makeValidationSuccess() {
+        setValidationResult(
+            NETWORK_VALIDATION_RESULT_VALID,
+            probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS,
+            probesSucceeded = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS
+        )
+        cm.reportNetworkConnectivity(network, true)
+        // Ensure validation is scheduled
+        waitForIdle()
+        // Ensure validation completes on mock executor
+        waitForIdleSerialExecutor(CSTestExecutor, HANDLER_TIMEOUT_MS)
+        // Ensure validation results are processed
+        waitForIdle()
+    }
+
+    private fun setMobileDataPreferredUids(uids: Set<Int>) {
+        ConnectivitySettingsManager.setMobileDataPreferredUids(context, uids)
+        service.updateMobileDataPreferredUids()
+        waitForIdle()
+    }
+}
+
+private fun makeInternetNc() = NetworkCapabilities.Builder(defaultNc())
+    .addCapability(NET_CAPABILITY_INTERNET)
+    .build()
+
+private fun CSAgentWrapper.sendLpChange(
+    mutator: LinkProperties.() -> Unit
+): LinkProperties.() -> Unit {
+    lp.mutator()
+    sendLinkProperties(lp)
+    return mutator
+}
+
+private fun CSAgentWrapper.sendNcChange(
+    mutator: NetworkCapabilities.() -> Unit
+): NetworkCapabilities.() -> Unit {
+    nc.mutator()
+    sendNetworkCapabilities(nc)
+    return mutator
+}
+
+private fun TestableNetworkCallback.expectLpWith(
+    agent: CSAgentWrapper,
+    change: LinkProperties.() -> Unit
+) = expect<LinkPropertiesChanged>(agent) {
+    // This test uses changes that are no-op when already applied (idempotent): verify that the
+    // change is already applied.
+    it.lp == LinkProperties(it.lp).apply(change)
+}
+
+private fun TestableNetworkCallback.expectNcWith(
+    agent: CSAgentWrapper,
+    change: NetworkCapabilities.() -> Unit
+) = expect<CapabilitiesChanged>(agent) {
+    it.caps == NetworkCapabilities(it.caps).apply(change)
+}
+
+private fun LinkProperties.setLinkAddresses(vararg addrs: String) {
+    setLinkAddresses(addrs.map { LinkAddress(it) })
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
similarity index 69%
rename from tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
rename to tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 9024641..88c2738 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -24,6 +24,7 @@
 import android.net.NativeNetworkType
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
@@ -41,10 +42,13 @@
 import android.util.ArraySet
 import com.android.net.module.util.CollectionUtils
 import com.android.server.ConnectivityService.PREFERENCE_ORDER_SATELLITE_FALLBACK
+import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.visibleOnHandlerThread
 import org.junit.Assert
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
@@ -61,7 +65,10 @@
 @DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
-class CSSatelliteNetworkPreferredTest : CSTest() {
+class CSSatelliteNetworkTest : CSTest() {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
     /**
      * Test createMultiLayerNrisFromSatelliteNetworkPreferredUids returns correct
      * NetworkRequestInfo.
@@ -80,54 +87,81 @@
     }
 
     /**
-     * Test that SATELLITE_NETWORK_PREFERENCE_UIDS changes will send correct net id and uid ranges
-     * to netd.
+     * Test that satellite network satisfies satellite fallback per-app default network request and
+     * send correct net id and uid ranges to netd.
      */
-    @Test
-    fun testSatelliteNetworkPreferredUidsChanged() {
+    private fun doTestSatelliteNetworkFallbackUids(restricted: Boolean) {
         val netdInOrder = inOrder(netd)
 
-        val satelliteAgent = createSatelliteAgent("satellite0")
+        val satelliteAgent = createSatelliteAgent("satellite0", restricted)
         satelliteAgent.connect()
 
         val satelliteNetId = satelliteAgent.network.netId
+        val permission = if (restricted) {INetd.PERMISSION_SYSTEM} else {INetd.PERMISSION_NONE}
         netdInOrder.verify(netd).networkCreate(
-            nativeNetworkConfigPhysical(satelliteNetId, INetd.PERMISSION_NONE))
+            nativeNetworkConfigPhysical(satelliteNetId, permission))
 
         val uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
         val uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2)
         val uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
 
-        // Initial satellite network preferred uids status.
-        setAndUpdateSatelliteNetworkPreferredUids(setOf())
+        // Initial satellite network fallback uids status.
+        updateSatelliteNetworkFallbackUids(setOf())
         netdInOrder.verify(netd, never()).networkAddUidRangesParcel(any())
         netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
 
-        // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting and verify that net id and uid ranges
-        // send to netd
+        // Update satellite network fallback uids and verify that net id and uid ranges send to netd
         var uids = mutableSetOf(uid1, uid2, uid3)
         val uidRanges1 = toUidRangeStableParcels(uidRangesForUids(uids))
         val config1 = NativeUidRangeConfig(
             satelliteNetId, uidRanges1,
             PREFERENCE_ORDER_SATELLITE_FALLBACK
         )
-        setAndUpdateSatelliteNetworkPreferredUids(uids)
+        updateSatelliteNetworkFallbackUids(uids)
         netdInOrder.verify(netd).networkAddUidRangesParcel(config1)
         netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
 
-        // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting again and verify that old rules are removed
-        // and new rules are added.
+        // Update satellite network fallback uids and verify that net id and uid ranges send to netd
         uids = mutableSetOf(uid1)
         val uidRanges2: Array<UidRangeParcel?> = toUidRangeStableParcels(uidRangesForUids(uids))
         val config2 = NativeUidRangeConfig(
             satelliteNetId, uidRanges2,
             PREFERENCE_ORDER_SATELLITE_FALLBACK
         )
-        setAndUpdateSatelliteNetworkPreferredUids(uids)
+        updateSatelliteNetworkFallbackUids(uids)
         netdInOrder.verify(netd).networkRemoveUidRangesParcel(config1)
         netdInOrder.verify(netd).networkAddUidRangesParcel(config2)
     }
 
+    @Test
+    fun testSatelliteNetworkFallbackUids_restricted() {
+        doTestSatelliteNetworkFallbackUids(restricted = true)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testSatelliteNetworkFallbackUids_nonRestricted() {
+        doTestSatelliteNetworkFallbackUids(restricted = false)
+    }
+
+    private fun doTestSatelliteNeverBecomeDefaultNetwork(restricted: Boolean) {
+        val agent = createSatelliteAgent("satellite0", restricted)
+        agent.connect()
+        val defaultCb = TestableNetworkCallback()
+        cm.registerDefaultNetworkCallback(defaultCb)
+        // Satellite network must not become the default network
+        defaultCb.assertNoCallback()
+    }
+
+    @Test
+    fun testSatelliteNeverBecomeDefaultNetwork_restricted() {
+        doTestSatelliteNeverBecomeDefaultNetwork(restricted = true)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testSatelliteNeverBecomeDefaultNetwork_notRestricted() {
+        doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
+    }
+
     private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
         val nris: Set<ConnectivityService.NetworkRequestInfo> =
             service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
@@ -140,7 +174,7 @@
         assertEquals(PREFERENCE_ORDER_SATELLITE_FALLBACK, nri.mPreferenceOrder)
     }
 
-    private fun setAndUpdateSatelliteNetworkPreferredUids(uids: Set<Int>) {
+    private fun updateSatelliteNetworkFallbackUids(uids: Set<Int>) {
         visibleOnHandlerThread(csHandler) {
             deps.satelliteNetworkFallbackUidUpdate!!.accept(uids)
         }
@@ -150,9 +184,9 @@
         NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
             false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
 
-    private fun createSatelliteAgent(name: String): CSAgentWrapper {
+    private fun createSatelliteAgent(name: String, restricted: Boolean = true): CSAgentWrapper {
         return Agent(score = keepScore(), lp = lp(name),
-            nc = nc(TRANSPORT_SATELLITE, NET_CAPABILITY_INTERNET)
+            nc = satelliteNc(restricted)
         )
     }
 
@@ -176,17 +210,19 @@
         return uidRangesForUids(*CollectionUtils.toIntArray(uids))
     }
 
-    private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
-        addTransportType(transport)
-        caps.forEach {
-            addCapability(it)
-        }
-        // Useful capabilities for everybody
-        addCapability(NET_CAPABILITY_NOT_RESTRICTED)
-        addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-        addCapability(NET_CAPABILITY_NOT_ROAMING)
-        addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-    }.build()
+    private fun satelliteNc(restricted: Boolean) =
+            NetworkCapabilities.Builder().apply {
+                addTransportType(TRANSPORT_SATELLITE)
+
+                addCapability(NET_CAPABILITY_INTERNET)
+                addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                addCapability(NET_CAPABILITY_NOT_ROAMING)
+                addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                if (restricted) {
+                    removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                }
+                removeCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+            }.build()
 
     private fun lp(iface: String) = LinkProperties().apply {
         interfaceName = iface
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index d7343b1..1f5ee32 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -28,6 +28,7 @@
 import android.net.NetworkAgent
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkProvider
@@ -39,6 +40,9 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.test.assertEquals
+import kotlin.test.fail
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyInt
@@ -46,9 +50,6 @@
 import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.verify
 import org.mockito.stubbing.Answer
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.test.assertEquals
-import kotlin.test.fail
 
 const val SHORT_TIMEOUT_MS = 200L
 
@@ -140,6 +141,9 @@
         val request = NetworkRequest.Builder().apply {
             clearCapabilities()
             if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+            if (nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+                addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+            }
         }.build()
         val cb = TestableNetworkCallback()
         mgr.registerNetworkCallback(request, cb)
@@ -166,6 +170,9 @@
         val request = NetworkRequest.Builder().apply {
             clearCapabilities()
             if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+            if (nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+                addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+            }
         }.build()
         val cb = TestableNetworkCallback(timeoutMs = SHORT_TIMEOUT_MS)
         mgr.registerNetworkCallback(request, cb)
@@ -178,13 +185,15 @@
 
     fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
     fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
+    fun sendLinkProperties(lp: LinkProperties) = agent.sendLinkProperties(lp)
 
     fun connectWithCaptivePortal(redirectUrl: String) {
         setCaptivePortal(redirectUrl)
         connect()
     }
 
-    fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
+    fun setValidationResult(result: Int, probesCompleted: Int, probesSucceeded: Int) {
+        nmValidationResult = result
         nmProbesCompleted = probesCompleted
         nmProbesSucceeded = probesSucceeded
     }
@@ -196,8 +205,10 @@
         // in the beginning. Because NETWORK_VALIDATION_PROBE_HTTP is the decisive probe for captive
         // portal, considering the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet and set only
         // DNS and HTTP probes completed.
-        setProbesStatus(
-            NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP /* probesCompleted */,
-            VALIDATION_RESULT_INVALID /* probesSucceeded */)
+        setValidationResult(
+            VALIDATION_RESULT_INVALID,
+            probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP,
+            probesSucceeded = NO_PROBE_RESULT
+        )
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 595ca47..46c25d2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -17,6 +17,7 @@
 package com.android.server
 
 import android.app.AlarmManager
+import android.app.AppOpsManager
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
@@ -26,6 +27,7 @@
 import android.content.res.Resources
 import android.net.ConnectivityManager
 import android.net.INetd
+import android.net.INetd.PERMISSION_INTERNET
 import android.net.InetAddresses
 import android.net.LinkProperties
 import android.net.LocalNetworkConfig
@@ -42,6 +44,7 @@
 import android.net.NetworkProvider
 import android.net.NetworkScore
 import android.net.PacProxyManager
+import android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK
 import android.net.networkstack.NetworkStackClientBase
 import android.os.BatteryStatsManager
 import android.os.Bundle
@@ -51,9 +54,9 @@
 import android.os.UserHandle
 import android.os.UserManager
 import android.permission.PermissionManager.PermissionResult
+import android.telephony.SubscriptionManager
 import android.telephony.TelephonyManager
 import android.testing.TestableContext
-import android.util.ArraySet
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.internal.app.IBatteryStats
 import com.android.internal.util.test.BroadcastInterceptingContext
@@ -75,14 +78,19 @@
 import java.util.concurrent.Executors
 import java.util.concurrent.LinkedBlockingQueue
 import java.util.concurrent.TimeUnit
-import java.util.function.Consumer
 import java.util.function.BiConsumer
+import java.util.function.Consumer
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlin.annotation.AnnotationTarget.FUNCTION
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.fail
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
+import org.junit.rules.TestName
 import org.mockito.AdditionalAnswers.delegatesTo
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
@@ -103,6 +111,8 @@
 internal const val VERSION_V = 5
 internal const val VERSION_MAX = VERSION_V
 
+internal const val CALLING_UID_UNMOCKED = Process.INVALID_UID
+
 private fun NetworkCapabilities.getLegacyType() =
         when (transportTypes.getOrElse(0) { TRANSPORT_WIFI }) {
             TRANSPORT_BLUETOOTH -> ConnectivityManager.TYPE_BLUETOOTH
@@ -122,14 +132,19 @@
 // TODO (b/272685721) : make ConnectivityServiceTest smaller and faster by moving the setup
 // parts into this class and moving the individual tests to multiple separate classes.
 open class CSTest {
+    @get:Rule
+    val testNameRule = TestName()
+
     companion object {
         val CSTestExecutor = Executors.newSingleThreadExecutor()
     }
 
     init {
         if (!SdkLevel.isAtLeastS()) {
-            throw UnsupportedApiLevelException("CSTest subclasses must be annotated to only " +
-                    "run on S+, e.g. @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)")
+            throw UnsupportedApiLevelException(
+                "CSTest subclasses must be annotated to only " +
+                    "run on S+, e.g. @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)"
+            )
         }
     }
 
@@ -144,13 +159,15 @@
         it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = true
         it[ConnectivityFlags.REQUEST_RESTRICTED_WIFI] = true
         it[ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION] = true
-        it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
         it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
-        it[ConnectivityService.LOG_BPF_RC] = true
         it[ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK] = true
+        it[ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING] = true
+        it[ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN] = true
+        it[ConnectivityFlags.DELAY_DESTROY_SOCKETS] = true
+        it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
+        it[ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS] = true
     }
-    fun enableFeature(f: String) = enabledFeatures.set(f, true)
-    fun disableFeature(f: String) = enabledFeatures.set(f, false)
+    fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
 
     // When adding new members, consider if it's not better to build the object in CSTestHelpers
     // to keep this file clean of implementation details. Generally, CSTestHelpers should only
@@ -158,7 +175,11 @@
     val contentResolver = makeMockContentResolver(context)
 
     val PRIMARY_USER = 0
-    val PRIMARY_USER_INFO = UserInfo(PRIMARY_USER, "" /* name */, UserInfo.FLAG_PRIMARY)
+    val PRIMARY_USER_INFO = UserInfo(
+            PRIMARY_USER,
+            "", // name
+            UserInfo.FLAG_PRIMARY
+    )
     val PRIMARY_USER_HANDLE = UserHandle(PRIMARY_USER)
     val userManager = makeMockUserManager(PRIMARY_USER_INFO, PRIMARY_USER_HANDLE)
     val activityManager = makeActivityManager()
@@ -170,19 +191,28 @@
     val connResources = makeMockConnResources(sysResources, packageManager)
 
     val netd = mock<INetd>()
-    val bpfNetMaps = mock<BpfNetMaps>()
+    val bpfNetMaps = mock<BpfNetMaps>().also {
+        doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
+    }
     val clatCoordinator = mock<ClatCoordinator>()
     val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
-    val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
+    val proxyTracker = ProxyTracker(
+            context,
+            mock<Handler>(),
+            16 // EVENT_PROXY_HAS_CHANGED
+    )
     val systemConfigManager = makeMockSystemConfigManager()
     val batteryStats = mock<IBatteryStats>()
     val batteryManager = BatteryStatsManager(batteryStats)
+    val appOpsManager = mock<AppOpsManager>()
     val telephonyManager = mock<TelephonyManager>().also {
         doReturn(true).`when`(it).isDataCapable()
     }
+    val subscriptionManager = mock<SubscriptionManager>()
 
     val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
     val satelliteAccessController = mock<SatelliteAccessController>()
+    val destroySocketsWrapper = mock<DestroySocketsWrapper>()
 
     val deps = CSDeps()
 
@@ -193,8 +223,32 @@
     lateinit var cm: ConnectivityManager
     lateinit var csHandler: Handler
 
+    // Tests can use this annotation to set flag values before constructing ConnectivityService
+    // e.g. @FeatureFlags([Flag(flagName1, true/false), Flag(flagName2, true/false)])
+    @Retention(RUNTIME)
+    @Target(FUNCTION)
+    annotation class FeatureFlags(val flags: Array<Flag>)
+
+    @Retention(RUNTIME)
+    @Target(FUNCTION)
+    annotation class Flag(val name: String, val enabled: Boolean)
+
     @Before
     fun setUp() {
+        // Set feature flags before constructing ConnectivityService
+        val testMethodName = testNameRule.methodName
+        try {
+            val testMethod = this::class.java.getMethod(testMethodName)
+            val featureFlags = testMethod.getAnnotation(FeatureFlags::class.java)
+            if (featureFlags != null) {
+                for (flag in featureFlags.flags) {
+                    setFeatureEnabled(flag.name, flag.enabled)
+                }
+            }
+        } catch (ignored: NoSuchMethodException) {
+            // This is expected for parameterized tests
+        }
+
         alarmHandlerThread = HandlerThread("TestAlarmManager").also { it.start() }
         alarmManager = makeMockAlarmManager(alarmHandlerThread)
         service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
@@ -212,6 +266,11 @@
         alarmHandlerThread.join()
     }
 
+    // Class to be mocked and used to verify destroy sockets methods call
+    open inner class DestroySocketsWrapper {
+        open fun destroyLiveTcpSocketsByOwnerUids(ownerUids: Set<Int>) {}
+    }
+
     inner class CSDeps : ConnectivityService.Dependencies() {
         override fun getResources(ctx: Context) = connResources
         override fun getBpfNetMaps(context: Context, netd: INetd) = this@CSTest.bpfNetMaps
@@ -227,7 +286,8 @@
                 context: Context,
                 tm: TelephonyManager,
                 requestRestrictedWifiEnabled: Boolean,
-                listener: BiConsumer<Int, Int>
+                listener: BiConsumer<Int, Int>,
+                handler: Handler
         ) = if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
 
         var satelliteNetworkFallbackUidUpdate: Consumer<Set<Int>>? = null
@@ -249,8 +309,12 @@
                 AutomaticOnOffKeepaliveTracker(c, h, AOOKTDeps(c))
 
         override fun makeMultinetworkPolicyTracker(c: Context, h: Handler, r: Runnable) =
-                MultinetworkPolicyTracker(c, h, r,
-                        MultinetworkPolicyTrackerTestDependencies(connResources.get()))
+                MultinetworkPolicyTracker(
+                        c,
+                        h,
+                        r,
+                        MultinetworkPolicyTrackerTestDependencies(connResources.get())
+                )
 
         override fun makeNetworkRequestStateStatsMetrics(c: Context) =
                 this@CSTest.networkRequestStateStatsMetrics
@@ -264,7 +328,7 @@
                 enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
 
         // Mocked change IDs
-        private val enabledChangeIds = ArraySet<Long>()
+        private val enabledChangeIds = arrayListOf(ENABLE_MATCH_LOCAL_NETWORK)
         fun setChangeIdEnabled(enabled: Boolean, changeId: Long) {
             // enabledChangeIds is read on the handler thread and maybe the test thread, so
             // make sure both threads see it before continuing.
@@ -299,6 +363,24 @@
         override fun isAtLeastT() = if (isSdkUnmocked) super.isAtLeastT() else sdkLevel >= VERSION_T
         override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U
         override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V
+
+        private var callingUid = CALLING_UID_UNMOCKED
+
+        fun unmockCallingUid() {
+            setCallingUid(CALLING_UID_UNMOCKED)
+        }
+
+        fun setCallingUid(callingUid: Int) {
+            visibleOnHandlerThread(csHandler) { this.callingUid = callingUid }
+        }
+
+        override fun getCallingUid() =
+                if (callingUid == CALLING_UID_UNMOCKED) super.getCallingUid() else callingUid
+
+        override fun destroyLiveTcpSocketsByOwnerUids(ownerUids: Set<Int>) {
+            // Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
+            destroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids)
+        }
     }
 
     inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -322,8 +404,12 @@
         override fun enforceCallingOrSelfPermission(permission: String, message: String?) {
             // If the permission result does not set in the mMockedPermissions, it will be
             // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
-            val granted = checkMockedPermission(permission, Process.myPid(), Process.myUid(),
-                PERMISSION_GRANTED)
+            val granted = checkMockedPermission(
+                permission,
+                Process.myPid(),
+                Process.myUid(),
+                PERMISSION_GRANTED
+            )
             if (!granted.equals(PERMISSION_GRANTED)) {
                 throw SecurityException("[Test] permission denied: " + permission)
             }
@@ -334,8 +420,12 @@
         override fun checkCallingOrSelfPermission(permission: String) =
             checkMockedPermission(permission, Process.myPid(), Process.myUid(), PERMISSION_GRANTED)
 
-        private fun checkMockedPermission(permission: String, pid: Int, uid: Int, default: Int):
-                Int {
+        private fun checkMockedPermission(
+                permission: String,
+                pid: Int,
+                uid: Int,
+                default: Int
+        ): Int {
             val processSpecificKey = "$permission,$pid,$uid"
             return mMockedPermissions[processSpecificKey]
                     ?: mMockedPermissions[permission] ?: default
@@ -397,16 +487,17 @@
             Context.ACTIVITY_SERVICE -> activityManager
             Context.SYSTEM_CONFIG_SERVICE -> systemConfigManager
             Context.TELEPHONY_SERVICE -> telephonyManager
+            Context.TELEPHONY_SUBSCRIPTION_SERVICE -> subscriptionManager
             Context.BATTERY_STATS_SERVICE -> batteryManager
             Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
+            Context.APP_OPS_SERVICE -> appOpsManager
             else -> super.getSystemService(serviceName)
         }
 
         internal val orderedBroadcastAsUserHistory = ArrayTrackRecord<Intent>().newReadHead()
 
         fun expectNoDataActivityBroadcast(timeoutMs: Int) {
-            assertNull(orderedBroadcastAsUserHistory.poll(
-                    timeoutMs.toLong()) { intent -> true })
+            assertNull(orderedBroadcastAsUserHistory.poll(timeoutMs.toLong()))
         }
 
         override fun sendOrderedBroadcastAsUser(
@@ -447,8 +538,12 @@
             provider: NetworkProvider? = null
     ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
             nac, nc, lp, lnc, score, provider)
-    fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
-        val nc = NetworkCapabilities.Builder().apply {
+    fun Agent(
+        vararg transports: Int,
+        baseNc: NetworkCapabilities = defaultNc(),
+        lp: LinkProperties = defaultLp()
+    ): CSAgentWrapper {
+        val nc = NetworkCapabilities.Builder(baseNc).apply {
             transports.forEach {
                 addTransportType(it)
             }
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt b/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
new file mode 100644
index 0000000..c8b2f65
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// ktlint does not allow annotating function argument literals inline. Disable the specific rule
+// since this negatively affects readability.
+@file:Suppress("ktlint:standard:comment-wrapping")
+
+package com.android.server.ethernet
+
+import android.content.Context
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.os.Build
+import android.os.Handler
+import android.os.test.TestLooper
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val IFACE = "eth0"
+private val CAPS = NetworkCapabilities.Builder().build()
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class EthernetInterfaceStateMachineTest {
+    private lateinit var looper: TestLooper
+    private lateinit var handler: Handler
+    private lateinit var ifaceState: EthernetInterfaceStateMachine
+
+    @Mock private lateinit var context: Context
+    @Mock private lateinit var provider: NetworkProvider
+    @Mock private lateinit var deps: EthernetNetworkFactory.Dependencies
+
+    // There seems to be no (obvious) way to force execution of @Before and @Test annotation on the
+    // same thread. Since SyncStateMachine requires all interactions to be called from the same
+    // thread that is provided at construction time (in this case, the thread that TestLooper() is
+    // called on), setUp() must be called directly from the @Test method.
+    // TODO: find a way to fix this in the test runner.
+    fun setUp() {
+        looper = TestLooper()
+        handler = Handler(looper.looper)
+        MockitoAnnotations.initMocks(this)
+
+        ifaceState = EthernetInterfaceStateMachine(IFACE, handler, context, CAPS, provider, deps)
+    }
+
+    @Test
+    fun testUpdateLinkState_networkOfferRegisteredAndRetracted() {
+        setUp()
+
+        ifaceState.updateLinkState(/* up= */ true)
+
+        // link comes up: validate the NetworkOffer is registered and capture callback object.
+        val inOrder = inOrder(provider)
+        val networkOfferCb = ArgumentCaptor.forClass(NetworkOfferCallback::class.java).also {
+            inOrder.verify(provider).registerNetworkOffer(any(), any(), any(), it.capture())
+        }.value
+
+        ifaceState.updateLinkState(/* up */ false)
+
+        // link goes down: validate the NetworkOffer is retracted
+        inOrder.verify(provider).unregisterNetworkOffer(eq(networkOfferCb))
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 949e0c2..70d4ad8 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -554,4 +554,22 @@
         mNetworkOfferCallback.onNetworkNeeded(createDefaultRequest());
         verify(mIpClient, never()).startProvisioning(any());
     }
+
+    @Test
+    public void testGetMacAddressProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        final String result = mNetFactory.getHwAddress(TEST_IFACE);
+        assertEquals(HW_ADDR, result);
+    }
+
+    @Test
+    public void testGetMacAddressForNonExistingInterface() {
+        initEthernetNetworkFactory();
+
+        final String result = mNetFactory.getHwAddress(TEST_IFACE);
+        // No interface exists due to not calling createAndVerifyProvisionedInterface(...).
+        assertNull(result);
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 2be74db..3d2f389 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -56,6 +56,7 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.net.TrafficStats.UID_REMOVED;
 import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
 import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
@@ -68,16 +69,22 @@
 import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
 import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_UPDATED;
+import static com.android.server.net.NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG;
+import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
+import static com.android.server.net.NetworkStatsService.DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
 import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
 import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME;
 import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
 import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
 import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.AdditionalMatchers.aryEq;
@@ -97,8 +104,10 @@
 
 import android.annotation.NonNull;
 import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.ContentObserver;
@@ -118,6 +127,7 @@
 import android.net.TestNetworkSpecifier;
 import android.net.TetherStatsParcel;
 import android.net.TetheringManager;
+import android.net.TrafficStats;
 import android.net.UnderlyingNetworkInfo;
 import android.net.netstats.provider.INetworkStatsProviderCallback;
 import android.net.wifi.WifiInfo;
@@ -133,6 +143,7 @@
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.telephony.TelephonyManager;
+import android.testing.TestableLooper;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
@@ -145,6 +156,7 @@
 import com.android.connectivity.resources.R;
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.ArrayTrackRecord;
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.IBpfMap;
 import com.android.net.module.util.LocationPermissionChecker;
@@ -163,12 +175,15 @@
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TestBpfMap;
 import com.android.testutils.TestableNetworkStatsProviderBinder;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule;
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag;
 
 import libcore.testing.io.TestIoUtils;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -193,6 +208,7 @@
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
 
 /**
  * Tests for {@link NetworkStatsService}.
@@ -206,6 +222,7 @@
 // NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
 @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
 public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
+
     private static final String TAG = "NetworkStatsServiceTest";
 
     private static final long TEST_START = 1194220800000L;
@@ -300,6 +317,17 @@
     private Boolean mIsDebuggable;
     private HandlerThread mObserverHandlerThread;
     final TestDependencies mDeps = new TestDependencies();
+    final HashMap<String, Boolean> mFeatureFlags = new HashMap<>();
+    final HashMap<Long, Boolean> mCompatChanges = new HashMap<>();
+
+    // This will set feature flags from @FeatureFlag annotations
+    // into the map before setUp() runs.
+    @Rule
+    public final SetFeatureFlagsRule mSetFeatureFlagsRule =
+            new SetFeatureFlagsRule((name, enabled) -> {
+                mFeatureFlags.put(name, enabled);
+                return null;
+            }, (name) -> mFeatureFlags.getOrDefault(name, false));
 
     private class MockContext extends BroadcastInterceptingContext {
         private final Context mBaseContext;
@@ -410,8 +438,6 @@
 
         mElapsedRealtime = 0L;
 
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
         // Verify that system ready fetches realtime stats
@@ -450,6 +476,7 @@
 
     class TestDependencies extends NetworkStatsService.Dependencies {
         private int mCompareStatsInvocation = 0;
+        private NetworkStats.Entry mMockedTrafficStatsNativeStat = null;
 
         @Override
         public File getLegacyStatsDir() {
@@ -591,6 +618,57 @@
         public boolean supportEventLogger(@NonNull Context cts) {
             return true;
         }
+
+        @Override
+        public boolean alwaysUseTrafficStatsRateLimitCache(Context ctx) {
+            return mFeatureFlags.getOrDefault(TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, false);
+        }
+
+        @Override
+        public boolean enabledBroadcastNetworkStatsUpdatedRateLimiting(Context ctx) {
+            return mFeatureFlags.getOrDefault(
+                    BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, true);
+        }
+
+        @Override
+        public int getTrafficStatsRateLimitCacheExpiryDuration() {
+            return DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS;
+        }
+
+        @Override
+        public int getTrafficStatsRateLimitCacheMaxEntries() {
+            return DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES;
+        }
+
+        @Override
+        public boolean isChangeEnabled(long changeId, int uid) {
+            return mCompatChanges.getOrDefault(changeId, true);
+        }
+
+        public void setChangeEnabled(long changeId, boolean enabled) {
+            mCompatChanges.put(changeId, enabled);
+        }
+        @Nullable
+        @Override
+        public NetworkStats.Entry nativeGetTotalStat() {
+            return mMockedTrafficStatsNativeStat;
+        }
+
+        @Nullable
+        @Override
+        public NetworkStats.Entry nativeGetIfaceStat(String iface) {
+            return mMockedTrafficStatsNativeStat;
+        }
+
+        @Nullable
+        @Override
+        public NetworkStats.Entry nativeGetUidStat(int uid) {
+            return mMockedTrafficStatsNativeStat;
+        }
+
+        public void setNativeStat(NetworkStats.Entry entry) {
+            mMockedTrafficStatsNativeStat = entry;
+        }
     }
 
     @After
@@ -747,8 +825,6 @@
         assertStatsFilesExist(true);
 
         // boot through serviceReady() again
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
 
         mService.systemReady();
@@ -947,7 +1023,16 @@
     }
 
     @Test
-    public void testMobileStatsByRatType() throws Exception {
+    public void testMobileStatsByRatTypeForSatellite() throws Exception {
+        doTestMobileStatsByRatType(new NetworkStateSnapshot[]{buildSatelliteMobileState(IMSI_1)});
+    }
+
+    @Test
+    public void testMobileStatsByRatTypeForCellular() throws Exception {
+        doTestMobileStatsByRatType(new NetworkStateSnapshot[]{buildMobileState(IMSI_1)});
+    }
+
+    private void doTestMobileStatsByRatType(NetworkStateSnapshot[] states) throws Exception {
         final NetworkTemplate template3g = new NetworkTemplate.Builder(MATCH_MOBILE)
                 .setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
                 .setMeteredness(METERED_YES).build();
@@ -957,8 +1042,6 @@
         final NetworkTemplate template5g = new NetworkTemplate.Builder(MATCH_MOBILE)
                 .setRatType(TelephonyManager.NETWORK_TYPE_NR)
                 .setMeteredness(METERED_YES).build();
-        final NetworkStateSnapshot[] states =
-                new NetworkStateSnapshot[]{buildMobileState(IMSI_1)};
 
         // 3G network comes online.
         mockNetworkStatsSummary(buildEmptyStats());
@@ -972,7 +1055,7 @@
         incrementCurrentTime(MINUTE_IN_MILLIS);
         mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
-                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L)));
+                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L)));
         forcePollAndWaitForIdle();
 
         // Verify 3g templates gets stats.
@@ -987,7 +1070,7 @@
         mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
                 // Append more traffic on existing 3g stats entry.
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
-                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16L, 22L, 17L, 2L, 0L))
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16L, 22L, 17L, 2L, 0L))
                 // Add entry that is new on 4g.
                 .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
                         METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 33L, 27L, 8L, 10L, 1L)));
@@ -1389,6 +1472,57 @@
     }
 
     @Test
+    public void testGetUidStatsForTransportWithCellularAndSatellite() throws Exception {
+        // Setup satellite mobile network and Cellular mobile network
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
+
+        final NetworkStateSnapshot mobileState = buildStateOfTransport(
+                NetworkCapabilities.TRANSPORT_CELLULAR, TYPE_MOBILE,
+                TEST_IFACE2, IMSI_1, null /* wifiNetworkKey */,
+                false /* isTemporarilyNotMetered */, false /* isRoaming */);
+
+        final NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{mobileState,
+                buildSatelliteMobileState(IMSI_1)};
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+        setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_LTE);
+
+        // mock traffic on satellite network
+        final NetworkStats.Entry entrySatellite = new NetworkStats.Entry(
+                TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 80L, 5L, 70L, 15L, 1L);
+
+        // mock traffic on cellular network
+        final NetworkStats.Entry entryCellular = new NetworkStats.Entry(
+                TEST_IFACE2, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 100L, 15L, 150L, 15L, 1L);
+
+        final TetherStatsParcel[] emptyTetherStats = {};
+        // The interfaces that expect to be used to query the stats.
+        final String[] mobileIfaces = {TEST_IFACE, TEST_IFACE2};
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+                .insertEntry(entrySatellite).insertEntry(entryCellular), emptyTetherStats,
+                mobileIfaces);
+        // with getUidStatsForTransport(TRANSPORT_CELLULAR) return stats of both cellular
+        // and satellite
+        final NetworkStats mobileStats = mService.getUidStatsForTransport(
+                NetworkCapabilities.TRANSPORT_CELLULAR);
+
+        // The iface field of the returned stats should be null because getUidStatsForTransport
+        // clears the interface field before it returns the result.
+        assertValues(mobileStats, null /* iface */, UID_RED, SET_DEFAULT, TAG_NONE,
+                METERED_NO, ROAMING_NO, METERED_NO, 180L, 20L, 220L, 30L, 2L);
+
+        // getUidStatsForTransport(TRANSPORT_SATELLITE) is not supported
+        assertThrows(IllegalArgumentException.class,
+                () -> mService.getUidStatsForTransport(NetworkCapabilities.TRANSPORT_SATELLITE));
+
+    }
+
+    @Test
     public void testForegroundBackground() throws Exception {
         // pretend that network comes online
         mockDefaultSettings();
@@ -2071,8 +2205,6 @@
                 getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
 
         // Mock zero usage and boot through serviceReady(), verify there is no imported data.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
         assertStatsFilesExist(false);
@@ -2084,8 +2216,6 @@
         assertStatsFilesExist(false);
 
         // Boot through systemReady() again.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
 
@@ -2159,8 +2289,6 @@
                 getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
 
         // Mock zero usage and boot through serviceReady(), verify there is no imported data.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
         assertStatsFilesExist(false);
@@ -2172,8 +2300,6 @@
         assertStatsFilesExist(false);
 
         // Boot through systemReady() again.
-        mockDefaultSettings();
-        mockNetworkStatsUidDetail(buildEmptyStats());
         prepareForSystemReady();
         mService.systemReady();
 
@@ -2325,6 +2451,84 @@
         assertUidTotal(sTemplateWifi, UID_GREEN, 64L, 3L, 1024L, 8L, 0);
     }
 
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    public void testTrafficStatsRateLimitCache_disabledWithCompatChangeEnabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    public void testTrafficStatsRateLimitCache_enabledWithCompatChangeEnabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, true);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+    @Test
+    public void testTrafficStatsRateLimitCache_disabledWithCompatChangeDisabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+        doTestTrafficStatsRateLimitCache(false /* expectCached */);
+    }
+
+    @FeatureFlag(name = TRAFFICSTATS_RATE_LIMIT_CACHE_ENABLED_FLAG)
+    @Test
+    public void testTrafficStatsRateLimitCache_enabledWithCompatChangeDisabled() throws Exception {
+        mDeps.setChangeEnabled(ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE, false);
+        doTestTrafficStatsRateLimitCache(true /* expectCached */);
+    }
+
+    private void doTestTrafficStatsRateLimitCache(boolean expectCached) throws Exception {
+        mockDefaultSettings();
+        // Calling uid is not injected into the service, use the real uid to pass the caller check.
+        final int myUid = Process.myUid();
+        mockTrafficStatsValues(64L, 3L, 1024L, 8L);
+        assertTrafficStatsValues(TEST_IFACE, myUid, 64L, 3L, 1024L, 8L);
+
+        // Verify the values are cached.
+        incrementCurrentTime(DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS / 2);
+        mockTrafficStatsValues(65L, 8L, 1055L, 9L);
+        if (expectCached) {
+            assertTrafficStatsValues(TEST_IFACE, myUid, 64L, 3L, 1024L, 8L);
+        } else {
+            assertTrafficStatsValues(TEST_IFACE, myUid, 65L, 8L, 1055L, 9L);
+        }
+
+        // Verify the values are updated after cache expiry.
+        incrementCurrentTime(DEFAULT_TRAFFIC_STATS_CACHE_EXPIRY_DURATION_MS);
+        assertTrafficStatsValues(TEST_IFACE, myUid, 65L, 8L, 1055L, 9L);
+    }
+
+    private void mockTrafficStatsValues(long rxBytes, long rxPackets,
+            long txBytes, long txPackets) {
+        // In practice, keys and operations are not used and filled with default values when
+        // returned by JNI layer.
+        final NetworkStats.Entry entry = new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_DEFAULT,
+                TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO,
+                rxBytes, rxPackets, txBytes, txPackets, 0L);
+        mDeps.setNativeStat(entry);
+    }
+
+    // Assert for 3 different API return values respectively.
+    private void assertTrafficStatsValues(String iface, int uid, long rxBytes, long rxPackets,
+            long txBytes, long txPackets) {
+        assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
+                (type) -> mService.getTotalStats(type));
+        assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
+                (type) -> mService.getIfaceStats(iface, type));
+        assertTrafficStatsValuesThat(rxBytes, rxPackets, txBytes, txPackets,
+                (type) -> mService.getUidStats(uid, type));
+    }
+
+    private void assertTrafficStatsValuesThat(long rxBytes, long rxPackets, long txBytes,
+            long txPackets, Function<Integer, Long> fetcher) {
+        assertEquals(rxBytes, (long) fetcher.apply(TrafficStats.TYPE_RX_BYTES));
+        assertEquals(rxPackets, (long) fetcher.apply(TrafficStats.TYPE_RX_PACKETS));
+        assertEquals(txBytes, (long) fetcher.apply(TrafficStats.TYPE_TX_BYTES));
+        assertEquals(txPackets, (long) fetcher.apply(TrafficStats.TYPE_TX_PACKETS));
+    }
+
     private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
         assertEquals("shouldRunComparison (debuggable=" + isDebuggable + "): ",
                 expected, mService.shouldRunComparison());
@@ -2389,6 +2593,8 @@
     }
 
     private void prepareForSystemReady() throws Exception {
+        mockDefaultSettings();
+        mockNetworkStatsUidDetail(buildEmptyStats());
         mockNetworkStatsSummary(buildEmptyStats());
     }
 
@@ -2424,6 +2630,8 @@
 
     private void mockDefaultSettings() throws Exception {
         mockSettings(HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(
+                NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS);
     }
 
     private void mockSettings(long bucketDuration, long deleteAge) {
@@ -2438,6 +2646,8 @@
         @NonNull
         private volatile Config mConfig;
         private final AtomicBoolean mCombineSubtypeEnabled = new AtomicBoolean();
+        private long mBroadcastNetworkStatsUpdateDelayMs =
+                NetworkStatsService.BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
 
         TestNetworkStatsSettings(long bucketDuration, long deleteAge) {
             mConfig = new Config(bucketDuration, deleteAge, deleteAge);
@@ -2500,6 +2710,15 @@
         public boolean getAugmentEnabled() {
             return false;
         }
+
+        @Override
+        public long getBroadcastNetworkStatsUpdateDelayMs() {
+            return mBroadcastNetworkStatsUpdateDelayMs;
+        }
+
+        public void setBroadcastNetworkStatsUpdateDelayMs(long broadcastDelay) {
+            mBroadcastNetworkStatsUpdateDelayMs = broadcastDelay;
+        }
     }
 
     private void assertStatsFilesExist(boolean exist) {
@@ -2546,6 +2765,12 @@
                 false /* isTemporarilyNotMetered */, false /* isRoaming */);
     }
 
+    private static NetworkStateSnapshot buildSatelliteMobileState(String subscriberId) {
+        return buildStateOfTransport(NetworkCapabilities.TRANSPORT_SATELLITE, TYPE_MOBILE,
+                TEST_IFACE, subscriberId, null /* wifiNetworkKey */,
+                false /* isTemporarilyNotMetered */, false /* isRoaming */);
+    }
+
     private static NetworkStateSnapshot buildTestState(@NonNull String iface,
             @Nullable String wifiNetworkKey) {
         return buildStateOfTransport(NetworkCapabilities.TRANSPORT_TEST, TYPE_TEST,
@@ -2865,4 +3090,91 @@
         final String dump = getDump();
         assertDumpContains(dump, "Log for testing");
     }
+
+    private static class TestNetworkStatsUpdatedReceiver extends BroadcastReceiver {
+        private final ArrayTrackRecord<Intent>.ReadHead mHistory;
+
+        TestNetworkStatsUpdatedReceiver() {
+            mHistory = (new ArrayTrackRecord<Intent>()).newReadHead();
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mHistory.add(intent);
+        }
+
+        /**
+         * Assert no broadcast intent is received in blocking manner
+         */
+        public void assertNoBroadcastIntentReceived()  {
+            assertNull(mHistory.peek());
+        }
+
+        /**
+         * Assert an intent is received and remove it from queue
+         */
+        public void assertBroadcastIntentReceived() {
+            assertNotNull(mHistory.poll(WAIT_TIMEOUT, number -> true));
+        }
+    }
+
+    @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG)
+    @Test
+    public void testNetworkStatsUpdatedIntentSpam_rateLimitOn() throws Exception {
+        // Set the update delay long enough that messages won't be processed before unblocked
+        // Set a short time to test the behavior before reaching delay.
+        // Constraint: test running time < toleranceMs < update delay time
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+        final long toleranceMs = 5000;
+
+        final TestableLooper mTestableLooper = new TestableLooper(mHandlerThread.getLooper());
+        final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+        mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+        try {
+            // Test that before anything, the intent is delivered immediately
+            mService.forceUpdate();
+            mTestableLooper.processAllMessages();
+            receiver.assertBroadcastIntentReceived();
+            receiver.assertNoBroadcastIntentReceived();
+
+            // Test that the next two intents results in exactly one intent delivered
+            for (int i = 0; i < 2; i++) {
+                mService.forceUpdate();
+            }
+            // Test that the delay depends on our set value
+            mTestableLooper.moveTimeForward(mSettings.getBroadcastNetworkStatsUpdateDelayMs()
+                    - toleranceMs);
+            mTestableLooper.processAllMessages();
+            receiver.assertNoBroadcastIntentReceived();
+
+            // Unblock messages and test that the second and third update
+            // is broadcasted right after the delay
+            mTestableLooper.moveTimeForward(toleranceMs);
+            mTestableLooper.processAllMessages();
+            receiver.assertBroadcastIntentReceived();
+            receiver.assertNoBroadcastIntentReceived();
+
+        } finally {
+            mTestableLooper.destroy();
+        }
+    }
+
+    @FeatureFlag(name = BROADCAST_NETWORK_STATS_UPDATED_RATE_LIMIT_ENABLED_FLAG, enabled = false)
+    @Test
+    public void testNetworkStatsUpdatedIntentSpam_rateLimitOff() throws Exception {
+        // Set the update delay long enough to ensure that messages are processed
+        // despite the rate limit.
+        mSettings.setBroadcastNetworkStatsUpdateDelayMs(100_000L);
+
+        final TestNetworkStatsUpdatedReceiver receiver = new TestNetworkStatsUpdatedReceiver();
+        mServiceContext.registerReceiver(receiver, new IntentFilter(ACTION_NETWORK_STATS_UPDATED));
+
+        for (int i = 0; i < 2; i++) {
+            mService.forceUpdate();
+            waitForIdle();
+            receiver.assertBroadcastIntentReceived();
+        }
+        receiver.assertNoBroadcastIntentReceived();
+    }
 }
diff --git a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
index 27e6f96..99f762d 100644
--- a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -16,30 +16,35 @@
 
 package com.android.server.net
 
-import android.net.NetworkStats
+import android.net.NetworkStats.Entry
 import com.android.testutils.DevSdkIgnoreRunner
 import java.time.Clock
+import java.util.function.Supplier
 import kotlin.test.assertEquals
 import kotlin.test.assertNull
+import kotlin.test.fail
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 
 @RunWith(DevSdkIgnoreRunner::class)
 class TrafficStatsRateLimitCacheTest {
     companion object {
         private const val expiryDurationMs = 1000L
+        private const val maxSize = 2
     }
 
     private val clock = mock(Clock::class.java)
-    private val entry = mock(NetworkStats.Entry::class.java)
-    private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs)
+    private val entry = mock(Entry::class.java)
+    private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs, maxSize)
 
     @Test
     fun testGet_returnsEntryIfNotExpired() {
         cache.put("iface", 2, entry)
-        `when`(clock.millis()).thenReturn(500L) // Set clock to before expiry
+        doReturn(500L).`when`(clock).millis() // Set clock to before expiry
         val result = cache.get("iface", 2)
         assertEquals(entry, result)
     }
@@ -47,7 +52,7 @@
     @Test
     fun testGet_returnsNullIfExpired() {
         cache.put("iface", 2, entry)
-        `when`(clock.millis()).thenReturn(2000L) // Set clock to after expiry
+        doReturn(2000L).`when`(clock).millis() // Set clock to after expiry
         assertNull(cache.get("iface", 2))
     }
 
@@ -59,8 +64,8 @@
 
     @Test
     fun testPutAndGet_retrievesCorrectEntryForDifferentKeys() {
-        val entry1 = mock(NetworkStats.Entry::class.java)
-        val entry2 = mock(NetworkStats.Entry::class.java)
+        val entry1 = mock(Entry::class.java)
+        val entry2 = mock(Entry::class.java)
 
         cache.put("iface1", 2, entry1)
         cache.put("iface2", 4, entry2)
@@ -71,8 +76,8 @@
 
     @Test
     fun testPut_overridesExistingEntry() {
-        val entry1 = mock(NetworkStats.Entry::class.java)
-        val entry2 = mock(NetworkStats.Entry::class.java)
+        val entry1 = mock(Entry::class.java)
+        val entry2 = mock(Entry::class.java)
 
         cache.put("iface", 2, entry1)
         cache.put("iface", 2, entry2) // Put with the same key
@@ -81,6 +86,62 @@
     }
 
     @Test
+    fun testPut_removeLru() {
+        // Assumes max size is 2. Verify eldest entry get removed.
+        val entry1 = mock(Entry::class.java)
+        val entry2 = mock(Entry::class.java)
+        val entry3 = mock(Entry::class.java)
+
+        cache.put("iface1", 2, entry1)
+        cache.put("iface2", 4, entry2)
+        cache.put("iface3", 8, entry3)
+
+        assertNull(cache.get("iface1", 2))
+        assertEquals(entry2, cache.get("iface2", 4))
+        assertEquals(entry3, cache.get("iface3", 8))
+    }
+
+    @Test
+    fun testGetOrCompute_cacheHit() {
+        val entry1 = mock(Entry::class.java)
+
+        cache.put("iface1", 2, entry1)
+
+        // Set clock to before expiry.
+        doReturn(500L).`when`(clock).millis()
+
+        // Now call getOrCompute
+        val result = cache.getOrCompute("iface1", 2) {
+            fail("Supplier should not be called")
+        }
+
+        // Assertions
+        assertEquals(entry1, result) // Should get the cached entry.
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    @Test
+    fun testGetOrCompute_cacheMiss() {
+        val entry1 = mock(Entry::class.java)
+
+        cache.put("iface1", 2, entry1)
+
+        // Set clock to after expiry.
+        doReturn(1500L).`when`(clock).millis()
+
+        // Mock the supplier to return our network stats entry.
+        val supplier = mock(Supplier::class.java) as Supplier<Entry>
+        doReturn(entry1).`when`(supplier).get()
+
+        // Now call getOrCompute.
+        val result = cache.getOrCompute("iface1", 2, supplier)
+
+        // Assertions.
+        assertEquals(entry1, result) // Should get the cached entry.
+        verify(supplier).get()
+    }
+
+    @Test
     fun testClear() {
         cache.put("iface", 2, entry)
         cache.clear()
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
deleted file mode 100644
index f74eab8..0000000
--- a/tests/unit/vpn-jarjar-rules.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-# Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.connectivity.VpnProfileStore
diff --git a/thread/README.md b/thread/README.md
index f50e0cd..41b73ac 100644
--- a/thread/README.md
+++ b/thread/README.md
@@ -1,3 +1,18 @@
 # Thread
 
 Bring the [Thread](https://www.threadgroup.org/) networking protocol to Android.
+
+## Try Thread with Cuttlefish
+
+```
+# Get the code and go to the Android source code root directory
+
+source build/envsetup.sh
+lunch aosp_cf_x86_64_phone-trunk_staging-userdebug
+m
+
+launch_cvd
+```
+
+Open `https://localhost:8443/` in your web browser, you can find the Thread
+demoapp (with the Thread logo) in the cuttlefish instance. Open it and have fun with Thread!
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index ebbb9af..34d67bb 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -11,5 +11,8 @@
     }
   ],
   "postsubmit": [
+    {
+      "name": "ThreadNetworkMultiDeviceTests"
+    }
   ]
 }
diff --git a/thread/apex/ot-daemon.34rc b/thread/apex/ot-daemon.34rc
index 25060d1..86f6b69 100644
--- a/thread/apex/ot-daemon.34rc
+++ b/thread/apex/ot-daemon.34rc
@@ -21,5 +21,5 @@
     user thread_network
     group thread_network inet system
     seclabel u:r:ot_daemon:s0
-    socket ot-daemon/thread-wpan.sock stream 0666 thread_network thread_network
+    socket ot-daemon/thread-wpan.sock stream 0660 thread_network thread_network
     override
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
index fcfd469..117b4f9 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -34,7 +34,19 @@
     libs: [
         "framework-connectivity-t",
     ],
+    required: [
+        "privapp-permissions-com.android.threadnetwork.demoapp",
+    ],
+    system_ext_specific: true,
     certificate: "platform",
     privileged: true,
     platform_apis: true,
 }
+
+prebuilt_etc {
+    name: "privapp-permissions-com.android.threadnetwork.demoapp",
+    src: "privapp-permissions-com.android.threadnetwork.demoapp.xml",
+    sub_dir: "permissions",
+    filename_from_src: true,
+    system_ext_specific: true,
+}
diff --git a/thread/demoapp/privapp-permissions-com.android.threadnetwork.demoapp.xml b/thread/demoapp/privapp-permissions-com.android.threadnetwork.demoapp.xml
new file mode 100644
index 0000000..1995e60
--- /dev/null
+++ b/thread/demoapp/privapp-permissions-com.android.threadnetwork.demoapp.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- The privileged permissions needed by the com.android.threadnetwork.demoapp app. -->
+<permissions>
+    <privapp-permissions package="com.android.threadnetwork.demoapp">
+        <permission name="android.permission.THREAD_NETWORK_PRIVILEGED" />
+    </privapp-permissions>
+</permissions>
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
index 846253c..f8fe422 100644
--- a/thread/framework/Android.bp
+++ b/thread/framework/Android.bp
@@ -30,3 +30,14 @@
         "//packages/modules/Connectivity:__subpackages__",
     ],
 }
+
+filegroup {
+    name: "framework-thread-ot-daemon-shared-aidl-sources",
+    srcs: [
+        "java/android/net/thread/ChannelMaxPower.aidl",
+    ],
+    path: "java",
+    visibility: [
+        "//external/ot-br-posix:__subpackages__",
+    ],
+}
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
index b74a15a..22457f5 100644
--- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -18,7 +18,7 @@
 
 import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.internal.util.Preconditions.checkState;
-import static com.android.net.module.util.HexDump.dumpHexString;
+import static com.android.net.module.util.HexDump.toHexString;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
@@ -74,42 +74,61 @@
 public final class ActiveOperationalDataset implements Parcelable {
     /** The maximum length of the Active Operational Dataset TLV array in bytes. */
     public static final int LENGTH_MAX_DATASET_TLVS = 254;
+
     /** The length of Extended PAN ID in bytes. */
     public static final int LENGTH_EXTENDED_PAN_ID = 8;
+
     /** The minimum length of Network Name as UTF-8 bytes. */
     public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1;
+
     /** The maximum length of Network Name as UTF-8 bytes. */
     public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16;
+
     /** The length of Network Key in bytes. */
     public static final int LENGTH_NETWORK_KEY = 16;
+
     /** The length of Mesh-Local Prefix in bits. */
     public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64;
+
     /** The length of PSKc in bytes. */
     public static final int LENGTH_PSKC = 16;
+
     /** The 2.4 GHz channel page. */
     public static final int CHANNEL_PAGE_24_GHZ = 0;
+
     /** The minimum 2.4GHz channel. */
     public static final int CHANNEL_MIN_24_GHZ = 11;
+
     /** The maximum 2.4GHz channel. */
     public static final int CHANNEL_MAX_24_GHZ = 26;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_CHANNEL = 0;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_PAN_ID = 1;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_EXTENDED_PAN_ID = 2;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_NETWORK_NAME = 3;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_PSKC = 4;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_NETWORK_KEY = 5;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_MESH_LOCAL_PREFIX = 7;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_SECURITY_POLICY = 12;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_ACTIVE_TIMESTAMP = 14;
+
     /** @hide */
     @VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53;
 
@@ -591,7 +610,7 @@
         sb.append("{networkName=")
                 .append(getNetworkName())
                 .append(", extendedPanId=")
-                .append(dumpHexString(getExtendedPanId()))
+                .append(toHexString(getExtendedPanId()))
                 .append(", panId=")
                 .append(getPanId())
                 .append(", channel=")
@@ -975,8 +994,10 @@
     public static final class SecurityPolicy {
         /** The default Rotation Time in hours. */
         public static final int DEFAULT_ROTATION_TIME_HOURS = 672;
+
         /** The minimum length of Security Policy flags in bytes. */
         public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1;
+
         /** The length of Rotation Time TLV value in bytes. */
         private static final int LENGTH_SECURITY_POLICY_ROTATION_TIME = 2;
 
@@ -1088,7 +1109,7 @@
             sb.append("{rotation=")
                     .append(mRotationTimeHours)
                     .append(", flags=")
-                    .append(dumpHexString(mFlags))
+                    .append(toHexString(mFlags))
                     .append("}");
             return sb.toString();
         }
diff --git a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
similarity index 66%
copy from tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
copy to thread/framework/java/android/net/thread/ChannelMaxPower.aidl
index 472e347..abc00b9 100644
--- a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
+++ b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
@@ -14,8 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.cts.net.arguments;
+package android.net.thread;
 
-public interface InstrumentationArguments {
-    String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
+/**
+ * Mapping from a channel to its max power.
+ *
+ * {@hide}
+ */
+parcelable ChannelMaxPower {
+    int channel; // The Thread radio channel.
+    int maxPower; // The max power in the unit of 0.01dBm. Passing INT16_MAX(32767) will
+                  // disable the channel.
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
similarity index 64%
rename from tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java
rename to thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
index 6596269..dcc4545 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java
+++ b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.cts.net.hostside;
+package android.net.thread;
 
-import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+import android.net.thread.ThreadConfiguration;
 
-@RequiredProperties({NON_METERED_NETWORK})
-public class ExpeditedJobNonMeteredTest extends AbstractExpeditedJobTest {
+/** Receives the result of a Thread Configuration change. @hide */
+oneway interface IConfigurationReceiver {
+    void onConfigurationChanged(in ThreadConfiguration configuration);
 }
diff --git a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
index b576b33..3fece65 100644
--- a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
+++ b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
@@ -23,6 +23,8 @@
  * @hide
  */
 oneway interface IOperationalDatasetCallback {
-    void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
-    void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
+    void onActiveOperationalDatasetChanged(
+            in @nullable ActiveOperationalDataset activeOpDataset);
+    void onPendingOperationalDatasetChanged(
+            in @nullable PendingOperationalDataset pendingOpDataset);
 }
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 485e25d..b7f68c9 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -17,17 +17,20 @@
 package android.net.thread;
 
 import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ChannelMaxPower;
 import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IConfigurationReceiver;
 import android.net.thread.IOperationReceiver;
+import android.net.thread.IOperationalDatasetCallback;
 import android.net.thread.IScheduleMigrationReceiver;
 import android.net.thread.IStateCallback;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
 
 /**
-* Interface for communicating with ThreadNetworkControllerService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkControllerService.
+ * @hide
+ */
 interface IThreadNetworkController {
     void registerStateCallback(in IStateCallback callback);
     void unregisterStateCallback(in IStateCallback callback);
@@ -35,13 +38,19 @@
     void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
 
     void join(in ActiveOperationalDataset activeOpDataset, in IOperationReceiver receiver);
-    void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
+    void scheduleMigration(
+            in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
     void leave(in IOperationReceiver receiver);
 
-    void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+    void setTestNetworkAsUpstream(
+            in String testNetworkInterfaceName, in IOperationReceiver receiver);
+    void setChannelMaxPowers(in ChannelMaxPower[] channelMaxPowers, in IOperationReceiver receiver);
 
     int getThreadVersion();
     void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
 
     void setEnabled(boolean enabled, in IOperationReceiver receiver);
+    void setConfiguration(in ThreadConfiguration config, in IOperationReceiver receiver);
+    void registerConfigurationCallback(in IConfigurationReceiver receiver);
+    void unregisterConfigurationCallback(in IConfigurationReceiver receiver);
 }
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
index 0e394b1..b63cd72 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
@@ -19,9 +19,9 @@
 import android.net.thread.IThreadNetworkController;
 
 /**
-* Interface for communicating with ThreadNetworkService.
-* @hide
-*/
+ * Interface for communicating with ThreadNetworkService.
+ * @hide
+ */
 interface IThreadNetworkManager {
     List<IThreadNetworkController> getAllThreadNetworkControllers();
 }
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
index 520acbd..cecb4e9 100644
--- a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -65,11 +65,32 @@
      */
     @NonNull
     public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
+        return OperationalDatasetTimestamp.fromInstant(instant, true /* isAuthoritativeSource */);
+    }
+
+    /**
+     * Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}.
+     *
+     * <p>The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to
+     * {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
+     * is set to {@code isAuthoritativeSource}.
+     *
+     * <p>Note that this conversion can lose precision and a value returned by {@link #toInstant}
+     * may not equal exactly the {@code instant}.
+     *
+     * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
+     *     0xffffffffffffL}
+     * @see toInstant
+     * @hide
+     */
+    @NonNull
+    public static OperationalDatasetTimestamp fromInstant(
+            @NonNull Instant instant, boolean isAuthoritativeSource) {
         int ticks = getRoundedTicks(instant.getNano());
         long seconds = instant.getEpochSecond() + ticks / TICKS_UPPER_BOUND;
         // the rounded ticks can be 0x8000 if instant.getNano() >= 999984742
         ticks = ticks % TICKS_UPPER_BOUND;
-        return new OperationalDatasetTimestamp(seconds, ticks, true /* isAuthoritativeSource */);
+        return new OperationalDatasetTimestamp(seconds, ticks, isAuthoritativeSource);
     }
 
     /**
diff --git a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
similarity index 73%
rename from tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
rename to thread/framework/java/android/net/thread/ThreadConfiguration.aidl
index 472e347..9473411 100644
--- a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-package com.android.cts.net.arguments;
+package android.net.thread;
 
-public interface InstrumentationArguments {
-    String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
-}
+parcelable ThreadConfiguration;
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
new file mode 100644
index 0000000..be2632c
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Data interface for Thread device configuration.
+ *
+ * <p>An example usage of creating a {@link ThreadConfiguration} that turns on NAT64 feature based
+ * on an existing {@link ThreadConfiguration}:
+ *
+ * <pre>{@code
+ * ThreadConfiguration config =
+ *     new ThreadConfiguration.Builder(existingConfig).setNat64Enabled(true).build();
+ * }</pre>
+ *
+ * @see ThreadNetworkController#setConfiguration
+ * @see ThreadNetworkController#registerConfigurationCallback
+ * @see ThreadNetworkController#unregisterConfigurationCallback
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+@SystemApi
+public final class ThreadConfiguration implements Parcelable {
+    private final boolean mNat64Enabled;
+    private final boolean mDhcpv6PdEnabled;
+
+    private ThreadConfiguration(Builder builder) {
+        this(builder.mNat64Enabled, builder.mDhcpv6PdEnabled);
+    }
+
+    private ThreadConfiguration(boolean nat64Enabled, boolean dhcpv6PdEnabled) {
+        this.mNat64Enabled = nat64Enabled;
+        this.mDhcpv6PdEnabled = dhcpv6PdEnabled;
+    }
+
+    /** Returns {@code true} if NAT64 is enabled. */
+    public boolean isNat64Enabled() {
+        return mNat64Enabled;
+    }
+
+    /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
+    public boolean isDhcpv6PdEnabled() {
+        return mDhcpv6PdEnabled;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof ThreadConfiguration)) {
+            return false;
+        } else {
+            ThreadConfiguration otherConfig = (ThreadConfiguration) other;
+            return mNat64Enabled == otherConfig.mNat64Enabled
+                    && mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mNat64Enabled, mDhcpv6PdEnabled);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append('{');
+        sb.append("Nat64Enabled=").append(mNat64Enabled);
+        sb.append(", Dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeBoolean(mNat64Enabled);
+        dest.writeBoolean(mDhcpv6PdEnabled);
+    }
+
+    public static final @NonNull Creator<ThreadConfiguration> CREATOR =
+            new Creator<>() {
+                @Override
+                public ThreadConfiguration createFromParcel(Parcel in) {
+                    ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
+                    builder.setNat64Enabled(in.readBoolean());
+                    builder.setDhcpv6PdEnabled(in.readBoolean());
+                    return builder.build();
+                }
+
+                @Override
+                public ThreadConfiguration[] newArray(int size) {
+                    return new ThreadConfiguration[size];
+                }
+            };
+
+    /**
+     * The builder for creating {@link ThreadConfiguration} objects.
+     *
+     * @hide
+     */
+    public static final class Builder {
+        private boolean mNat64Enabled = false;
+        private boolean mDhcpv6PdEnabled = false;
+
+        /** Creates a new {@link Builder} object with all features disabled. */
+        public Builder() {}
+
+        /**
+         * Creates a new {@link Builder} object from a {@link ThreadConfiguration} object.
+         *
+         * @param config the Border Router configurations to be copied
+         */
+        public Builder(@NonNull ThreadConfiguration config) {
+            Objects.requireNonNull(config);
+
+            mNat64Enabled = config.mNat64Enabled;
+            mDhcpv6PdEnabled = config.mDhcpv6PdEnabled;
+        }
+
+        /**
+         * Enables or disables NAT64 for the device.
+         *
+         * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
+         * IPv4.
+         */
+        @NonNull
+        public Builder setNat64Enabled(boolean enabled) {
+            this.mNat64Enabled = enabled;
+            return this;
+        }
+
+        /**
+         * Enables or disables Prefix Delegation for the device.
+         *
+         * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
+         * IPv6.
+         */
+        @NonNull
+        public Builder setDhcpv6PdEnabled(boolean enabled) {
+            this.mDhcpv6PdEnabled = enabled;
+            return this;
+        }
+
+        /** Creates a new {@link ThreadConfiguration} object. */
+        @NonNull
+        public ThreadConfiguration build() {
+            return new ThreadConfiguration(this);
+        }
+    }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index db761a3..b4e581c 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -25,10 +25,12 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.Size;
 import android.annotation.SystemApi;
 import android.os.Binder;
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
+import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -39,6 +41,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * Provides the primary APIs for controlling all aspects of a Thread network.
@@ -98,6 +101,12 @@
     /** Thread standard version 1.3. */
     public static final int THREAD_VERSION_1_3 = 4;
 
+    /** Minimum value of max power in unit of 0.01dBm. @hide */
+    private static final int POWER_LIMITATION_MIN = -32768;
+
+    /** Maximum value of max power in unit of 0.01dBm. @hide */
+    private static final int POWER_LIMITATION_MAX = 32767;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({THREAD_VERSION_1_3})
@@ -116,6 +125,12 @@
     private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
             mOpDatasetCallbackMap = new HashMap<>();
 
+    private final Object mConfigurationCallbackMapLock = new Object();
+
+    @GuardedBy("mConfigurationCallbackMapLock")
+    private final Map<Consumer<ThreadConfiguration>, ConfigurationCallbackProxy>
+            mConfigurationCallbackMap = new HashMap<>();
+
     /** @hide */
     public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
         requireNonNull(controllerService, "controllerService cannot be null");
@@ -571,6 +586,95 @@
     }
 
     /**
+     * Configures the Thread features for this device.
+     *
+     * <p>This method sets the {@link ThreadConfiguration} for this device. On success, the {@link
+     * OutcomeReceiver#onResult} will be called, and the {@code configuration} will be applied and
+     * persisted to the device; the configuration changes can be observed by {@link
+     * #registerConfigurationCallback}. On failure, {@link OutcomeReceiver#onError} of {@code
+     * receiver} will be invoked with a specific error.
+     *
+     * @param configuration the configuration to set
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive result of this operation
+     * @hide
+     */
+    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    public void setConfiguration(
+            @NonNull ThreadConfiguration configuration,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        requireNonNull(configuration, "Configuration cannot be null");
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(receiver, "receiver cannot be null");
+        try {
+            mControllerService.setConfiguration(
+                    configuration, new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers a callback to be called when the configuration is changed.
+     *
+     * <p>Upon return of this method, {@code callback} will be invoked immediately with the current
+     * {@link ThreadConfiguration}.
+     *
+     * @param executor the executor to execute the {@code callback}
+     * @param callback the callback to receive Thread configuration changes
+     * @throws IllegalArgumentException if {@code callback} has already been registered
+     */
+    @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    public void registerConfigurationCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ThreadConfiguration> callback) {
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(callback, "callback cannot be null");
+        synchronized (mConfigurationCallbackMapLock) {
+            if (mConfigurationCallbackMap.containsKey(callback)) {
+                throw new IllegalArgumentException("callback has already been registered");
+            }
+            ConfigurationCallbackProxy callbackProxy =
+                    new ConfigurationCallbackProxy(executor, callback);
+            mConfigurationCallbackMap.put(callback, callbackProxy);
+            try {
+                mControllerService.registerConfigurationCallback(callbackProxy);
+            } catch (RemoteException e) {
+                mConfigurationCallbackMap.remove(callback);
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Unregisters the configuration callback.
+     *
+     * @param callback the callback which has been registered with {@link
+     *     #registerConfigurationCallback}
+     * @throws IllegalArgumentException if {@code callback} hasn't been registered
+     */
+    @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
+        requireNonNull(callback, "callback cannot be null");
+        synchronized (mConfigurationCallbackMapLock) {
+            ConfigurationCallbackProxy callbackProxy = mConfigurationCallbackMap.get(callback);
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("callback hasn't been registered");
+            }
+            try {
+                mControllerService.unregisterConfigurationCallback(callbackProxy);
+                mConfigurationCallbackMap.remove(callbackProxy.mConfigurationConsumer);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
      * Sets to use a specified test network as the upstream.
      *
      * @param testNetworkInterfaceName The name of the test network interface. When it's null,
@@ -596,6 +700,98 @@
         }
     }
 
+    /**
+     * Sets max power of each channel.
+     *
+     * <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
+     * chip firmware.
+     *
+     * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
+     * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; When failed, {@link
+     * OutcomeReceiver#onError} will be called with a specific error:
+     *
+     * <ul>
+     *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_OPERATION} the operation is no
+     *       supported by the platform.
+     * </ul>
+     *
+     * @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
+     *     and corresponding max power. Valid channel values should be between {@link
+     *     ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
+     *     ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. Max
+     *     power values should be between INT16_MIN (-32768) and INT16_MAX (32767). If the max power
+     *     is set to INT16_MAX, the corresponding channel is not supported.
+     * @param executor the executor to execute {@code receiver}.
+     * @param receiver the receiver to receive the result of this operation.
+     * @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
+     *     or invalid channel or max power is configured.
+     * @hide
+     */
+    @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+    public final void setChannelMaxPowers(
+            @NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        requireNonNull(channelMaxPowers, "channelMaxPowers cannot be null");
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(receiver, "receiver cannot be null");
+
+        if (channelMaxPowers.size() < 1) {
+            throw new IllegalArgumentException("channelMaxPowers cannot be empty");
+        }
+
+        for (int i = 0; i < channelMaxPowers.size(); i++) {
+            int channel = channelMaxPowers.keyAt(i);
+            int maxPower = channelMaxPowers.get(channel);
+
+            if ((channel < ActiveOperationalDataset.CHANNEL_MIN_24_GHZ)
+                    || (channel > ActiveOperationalDataset.CHANNEL_MAX_24_GHZ)) {
+                throw new IllegalArgumentException(
+                        "Channel "
+                                + channel
+                                + " exceeds allowed range ["
+                                + ActiveOperationalDataset.CHANNEL_MIN_24_GHZ
+                                + ", "
+                                + ActiveOperationalDataset.CHANNEL_MAX_24_GHZ
+                                + "]");
+            }
+
+            if ((maxPower < POWER_LIMITATION_MIN) || (maxPower > POWER_LIMITATION_MAX)) {
+                throw new IllegalArgumentException(
+                        "Channel power ({channel: "
+                                + channel
+                                + ", maxPower: "
+                                + maxPower
+                                + "}) exceeds allowed range ["
+                                + POWER_LIMITATION_MIN
+                                + ", "
+                                + POWER_LIMITATION_MAX
+                                + "]");
+            }
+        }
+
+        try {
+            mControllerService.setChannelMaxPowers(
+                    toChannelMaxPowerArray(channelMaxPowers),
+                    new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private static ChannelMaxPower[] toChannelMaxPowerArray(
+            @NonNull SparseIntArray channelMaxPowers) {
+        final ChannelMaxPower[] powerArray = new ChannelMaxPower[channelMaxPowers.size()];
+
+        for (int i = 0; i < channelMaxPowers.size(); i++) {
+            powerArray[i] = new ChannelMaxPower();
+            powerArray[i].channel = channelMaxPowers.keyAt(i);
+            powerArray[i].maxPower = channelMaxPowers.get(powerArray[i].channel);
+        }
+
+        return powerArray;
+    }
+
     private static <T> void propagateError(
             Executor executor,
             OutcomeReceiver<T, ThreadNetworkException> receiver,
@@ -664,4 +860,26 @@
             propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
         }
     }
+
+    private static final class ConfigurationCallbackProxy extends IConfigurationReceiver.Stub {
+        final Executor mExecutor;
+        final Consumer<ThreadConfiguration> mConfigurationConsumer;
+
+        ConfigurationCallbackProxy(
+                @CallbackExecutor Executor executor,
+                Consumer<ThreadConfiguration> ConfigurationConsumer) {
+            this.mExecutor = executor;
+            this.mConfigurationConsumer = ConfigurationConsumer;
+        }
+
+        @Override
+        public void onConfigurationChanged(ThreadConfiguration configuration) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mConfigurationConsumer.accept(configuration));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
 }
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index 4def0fb..f699c30 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -138,8 +138,17 @@
      */
     public static final int ERROR_THREAD_DISABLED = 12;
 
+    /**
+     * The operation failed because it is not supported by the platform. For example, some platforms
+     * may not support setting the target power of each channel. The caller should not retry and may
+     * return an error to the user.
+     *
+     * @hide
+     */
+    public static final int ERROR_UNSUPPORTED_OPERATION = 13;
+
     private static final int ERROR_MIN = ERROR_INTERNAL_ERROR;
-    private static final int ERROR_MAX = ERROR_THREAD_DISABLED;
+    private static final int ERROR_MAX = ERROR_UNSUPPORTED_OPERATION;
 
     private final int mErrorCode;
 
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
index e6ab988..691bbf5 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
@@ -27,5 +27,9 @@
     /** @hide */
     public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
 
+    /** @hide */
+    public static final String FLAG_CONFIGURATION_ENABLED =
+            "com.android.net.thread.flags.configuration_enabled";
+
     private ThreadNetworkFlags() {}
 }
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index c176bfa..e012d41 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -1,9 +1,35 @@
 #!/usr/bin/env bash
 
-SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+ANDROID_ROOT_DIR=$(
+    while [ ! -d ".repo" ] && [ "$PWD" != "/" ]; do cd ..; done
+    pwd
+)
 
-GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
-ANDROID_BP_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/build-tools/linux-x86/bin/bpfmt
+if [ ! -d "$ANDROID_ROOT_DIR/.repo" ]; then
+    echo "Error: The script has to run in an Android repo checkout"
+    exit 1
+fi
 
-$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
-$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
+GOOGLE_JAVA_FORMAT=$ANDROID_ROOT_DIR/prebuilts/tools/common/google-java-format/google-java-format
+ANDROID_BP_FORMAT=$ANDROID_ROOT_DIR/prebuilts/build-tools/linux-x86/bin/bpfmt
+AIDL_FORMAT=$ANDROID_ROOT_DIR/system/tools/aidl/aidl-format.sh
+
+CONNECTIVITY_DIR=$ANDROID_ROOT_DIR/packages/modules/Connectivity
+OPENTHREAD_DIR=$ANDROID_ROOT_DIR/external/openthread
+OTBR_POSIX_DIR=$ANDROID_ROOT_DIR/external/ot-br-posix
+
+ALLOWED_CODE_DIRS=($CONNECTIVITY_DIR $OPENTHREAD_DIR $OTBR_POSIX_DIR)
+CODE_DIR=$(git rev-parse --show-toplevel)
+
+if [[ ! " ${ALLOWED_CODE_DIRS[@]} " =~ " ${CODE_DIR} " ]]; then
+    echo "Error: The script has to run in the Git project Connectivity, openthread or ot-br-posix"
+    exit 1
+fi
+
+if [[ $CODE_DIR == $CONNECTIVITY_DIR ]]; then
+    CODE_DIR=$CODE_DIR"/thread"
+fi
+
+$GOOGLE_JAVA_FORMAT --aosp -i $(find $CODE_DIR -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $CODE_DIR -name "*.bp")
+$AIDL_FORMAT -w $(find $CODE_DIR -name "*.aidl")
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index 6e2fac1..a82a499 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -45,6 +45,9 @@
         "modules-utils-shell-command-handler",
         "net-utils-device-common",
         "net-utils-device-common-netlink",
+        // The required dependency net-utils-device-common-struct-base is in the classpath via
+        // framework-connectivity
+        "net-utils-device-common-struct",
         "ot-daemon-aidl-java",
     ],
     apex_available: ["com.android.tethering"],
diff --git a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
index e3b4e1a..43ff336 100644
--- a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
+++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
@@ -16,10 +16,12 @@
 
 package com.android.server.thread;
 
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
 import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
 
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.ThreadNetworkException;
 import android.os.RemoteException;
 
 import com.android.internal.annotations.GuardedBy;
@@ -73,6 +75,17 @@
         }
     }
 
+    public void onError(Throwable e) {
+        if (e instanceof ThreadNetworkException) {
+            ThreadNetworkException threadException = (ThreadNetworkException) e;
+            onError(threadException.getErrorCode(), threadException.getMessage());
+        } else if (e instanceof RemoteException) {
+            onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        } else {
+            throw new AssertionError(e);
+        }
+    }
+
     public void onError(int errorCode, String errorMessage) {
         synchronized (sPendingReceiversLock) {
             sPendingReceivers.remove(this);
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
index be54cbc..e72c9ee 100644
--- a/thread/service/java/com/android/server/thread/InfraInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -16,14 +16,30 @@
 
 package com.android.server.thread;
 
-import android.os.ParcelFileDescriptor;
+import static android.system.OsConstants.IPPROTO_IPV6;
+import static android.system.OsConstants.IPPROTO_RAW;
+import static android.system.OsConstants.IPV6_CHECKSUM;
+import static android.system.OsConstants.IPV6_MULTICAST_HOPS;
+import static android.system.OsConstants.IPV6_RECVHOPLIMIT;
+import static android.system.OsConstants.IPV6_RECVPKTINFO;
+import static android.system.OsConstants.IPV6_UNICAST_HOPS;
 
+import android.net.util.SocketUtils;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.FileDescriptor;
 import java.io.IOException;
 
 /** Controller for the infrastructure network interface. */
 public class InfraInterfaceController {
     private static final String TAG = "InfraIfController";
 
+    private static final int ENABLE = 1;
+    private static final int IPV6_CHECKSUM_OFFSET = 2;
+    private static final int HOP_LIMIT = 255;
+
     static {
         System.loadLibrary("service-thread-jni");
     }
@@ -37,8 +53,21 @@
      * @throws IOException when fails to create the socket.
      */
     public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
-        return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
+        ParcelFileDescriptor parcelFd =
+                ParcelFileDescriptor.adoptFd(nativeCreateFilteredIcmp6Socket());
+        FileDescriptor fd = parcelFd.getFileDescriptor();
+        try {
+            Os.setsockoptInt(fd, IPPROTO_RAW, IPV6_CHECKSUM, IPV6_CHECKSUM_OFFSET);
+            Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, ENABLE);
+            Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, ENABLE);
+            Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, HOP_LIMIT);
+            Os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, HOP_LIMIT);
+            SocketUtils.bindSocketToInterface(fd, infraInterfaceName);
+        } catch (ErrnoException e) {
+            throw new IOException("Failed to setsockopt for the ICMPv6 socket", e);
+        }
+        return parcelFd;
     }
 
-    private static native int nativeCreateIcmp6Socket(String interfaceName) throws IOException;
+    private static native int nativeCreateFilteredIcmp6Socket() throws IOException;
 }
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index c74c023..1447ff8 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -19,9 +19,15 @@
 import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
+import android.net.DnsResolver;
+import android.net.InetAddresses;
+import android.net.Network;
+import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.text.TextUtils;
@@ -30,14 +36,19 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
 import com.android.server.thread.openthread.INsdPublisher;
+import com.android.server.thread.openthread.INsdResolveHostCallback;
+import com.android.server.thread.openthread.INsdResolveServiceCallback;
 import com.android.server.thread.openthread.INsdStatusReceiver;
 
-import java.util.ArrayDeque;
+import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.util.ArrayList;
-import java.util.Deque;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Executor;
 
 /**
@@ -48,33 +59,38 @@
  *
  * <p>All the data members of this class MUST be accessed in the {@code mHandler}'s Thread except
  * {@code mHandler} itself.
- *
- * <p>TODO: b/323300118 - Remove the following mechanism when the race condition in NsdManager is
- * fixed.
- *
- * <p>There's always only one running registration job at any timepoint. All other pending jobs are
- * queued in {@code mRegistrationJobs}. When a registration job is complete (i.e. the according
- * method in {@link NsdManager.RegistrationListener} is called), it will start the next registration
- * job in the queue.
  */
 public final class NsdPublisher extends INsdPublisher.Stub {
-    // TODO: b/321883491 - specify network for mDNS operations
     private static final String TAG = NsdPublisher.class.getSimpleName();
+
+    // TODO: b/321883491 - specify network for mDNS operations
+    @Nullable private Network mNetwork;
     private final NsdManager mNsdManager;
+    private final DnsResolver mDnsResolver;
     private final Handler mHandler;
     private final Executor mExecutor;
     private final SparseArray<RegistrationListener> mRegistrationListeners = new SparseArray<>(0);
-    private final Deque<Runnable> mRegistrationJobs = new ArrayDeque<>();
+    private final SparseArray<DiscoveryListener> mDiscoveryListeners = new SparseArray<>(0);
+    private final SparseArray<ServiceInfoListener> mServiceInfoListeners = new SparseArray<>(0);
+    private final SparseArray<HostInfoListener> mHostInfoListeners = new SparseArray<>(0);
 
     @VisibleForTesting
-    public NsdPublisher(NsdManager nsdManager, Handler handler) {
+    public NsdPublisher(NsdManager nsdManager, DnsResolver dnsResolver, Handler handler) {
+        mNetwork = null;
         mNsdManager = nsdManager;
+        mDnsResolver = dnsResolver;
         mHandler = handler;
         mExecutor = runnable -> mHandler.post(runnable);
     }
 
     public static NsdPublisher newInstance(Context context, Handler handler) {
-        return new NsdPublisher(context.getSystemService(NsdManager.class), handler);
+        return new NsdPublisher(
+                context.getSystemService(NsdManager.class), DnsResolver.getInstance(), handler);
+    }
+
+    // TODO: b/321883491 - NsdPublisher should be disabled when mNetwork is null
+    public void setNetworkForHostResolution(@Nullable Network network) {
+        mNetwork = network;
     }
 
     @Override
@@ -87,13 +103,9 @@
             List<DnsTxtAttribute> txt,
             INsdStatusReceiver receiver,
             int listenerId) {
-        postRegistrationJob(
-                () -> {
-                    NsdServiceInfo serviceInfo =
-                            buildServiceInfoForService(
-                                    hostname, name, type, subTypeList, port, txt);
-                    registerInternal(serviceInfo, receiver, listenerId, "service");
-                });
+        NsdServiceInfo serviceInfo =
+                buildServiceInfoForService(hostname, name, type, subTypeList, port, txt);
+        mHandler.post(() -> registerInternal(serviceInfo, receiver, listenerId, "service"));
     }
 
     private static NsdServiceInfo buildServiceInfoForService(
@@ -119,6 +131,27 @@
         return serviceInfo;
     }
 
+    @Override
+    public void registerHost(
+            String name, List<String> addresses, INsdStatusReceiver receiver, int listenerId) {
+        NsdServiceInfo serviceInfo = buildServiceInfoForHost(name, addresses);
+        mHandler.post(() -> registerInternal(serviceInfo, receiver, listenerId, "host"));
+    }
+
+    private static NsdServiceInfo buildServiceInfoForHost(
+            String name, List<String> addressStrings) {
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+
+        serviceInfo.setHostname(name);
+        ArrayList<InetAddress> addresses = new ArrayList<>(addressStrings.size());
+        for (String addressString : addressStrings) {
+            addresses.add(InetAddresses.parseNumericAddress(addressString));
+        }
+        serviceInfo.setHostAddresses(addresses);
+
+        return serviceInfo;
+    }
+
     private void registerInternal(
             NsdServiceInfo serviceInfo,
             INsdStatusReceiver receiver,
@@ -144,7 +177,7 @@
     }
 
     public void unregister(INsdStatusReceiver receiver, int listenerId) {
-        postRegistrationJob(() -> unregisterInternal(receiver, listenerId));
+        mHandler.post(() -> unregisterInternal(receiver, listenerId));
     }
 
     public void unregisterInternal(INsdStatusReceiver receiver, int listenerId) {
@@ -171,6 +204,157 @@
         mNsdManager.unregisterService(registrationListener);
     }
 
+    @Override
+    public void discoverService(String type, INsdDiscoverServiceCallback callback, int listenerId) {
+        mHandler.post(() -> discoverServiceInternal(type, callback, listenerId));
+    }
+
+    private void discoverServiceInternal(
+            String type, INsdDiscoverServiceCallback callback, int listenerId) {
+        checkOnHandlerThread();
+        Log.i(
+                TAG,
+                "Discovering services."
+                        + " Listener ID: "
+                        + listenerId
+                        + ", service type: "
+                        + type);
+
+        DiscoveryListener listener = new DiscoveryListener(listenerId, type, callback);
+        mDiscoveryListeners.append(listenerId, listener);
+        DiscoveryRequest discoveryRequest =
+                new DiscoveryRequest.Builder(type).setNetwork(null).build();
+        mNsdManager.discoverServices(discoveryRequest, mExecutor, listener);
+    }
+
+    @Override
+    public void stopServiceDiscovery(int listenerId) {
+        mHandler.post(() -> stopServiceDiscoveryInternal(listenerId));
+    }
+
+    private void stopServiceDiscoveryInternal(int listenerId) {
+        checkOnHandlerThread();
+
+        DiscoveryListener listener = mDiscoveryListeners.get(listenerId);
+        if (listener == null) {
+            Log.w(
+                    TAG,
+                    "Failed to stop service discovery. Listener ID "
+                            + listenerId
+                            + ". The listener is null.");
+            return;
+        }
+
+        Log.i(TAG, "Stopping service discovery. Listener: " + listener);
+        mNsdManager.stopServiceDiscovery(listener);
+    }
+
+    @Override
+    public void resolveService(
+            String name, String type, INsdResolveServiceCallback callback, int listenerId) {
+        mHandler.post(() -> resolveServiceInternal(name, type, callback, listenerId));
+    }
+
+    private void resolveServiceInternal(
+            String name, String type, INsdResolveServiceCallback callback, int listenerId) {
+        checkOnHandlerThread();
+
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+        serviceInfo.setServiceName(name);
+        serviceInfo.setServiceType(type);
+        serviceInfo.setNetwork(null);
+        Log.i(
+                TAG,
+                "Resolving service."
+                        + " Listener ID: "
+                        + listenerId
+                        + ", service name: "
+                        + name
+                        + ", service type: "
+                        + type);
+
+        ServiceInfoListener listener = new ServiceInfoListener(serviceInfo, listenerId, callback);
+        mServiceInfoListeners.append(listenerId, listener);
+        mNsdManager.registerServiceInfoCallback(serviceInfo, mExecutor, listener);
+    }
+
+    @Override
+    public void stopServiceResolution(int listenerId) {
+        mHandler.post(() -> stopServiceResolutionInternal(listenerId));
+    }
+
+    private void stopServiceResolutionInternal(int listenerId) {
+        checkOnHandlerThread();
+
+        ServiceInfoListener listener = mServiceInfoListeners.get(listenerId);
+        if (listener == null) {
+            Log.w(
+                    TAG,
+                    "Failed to stop service resolution. Listener ID: "
+                            + listenerId
+                            + ". The listener is null.");
+            return;
+        }
+
+        Log.i(TAG, "Stopping service resolution. Listener: " + listener);
+
+        try {
+            mNsdManager.unregisterServiceInfoCallback(listener);
+        } catch (IllegalArgumentException e) {
+            Log.w(
+                    TAG,
+                    "Failed to stop the service resolution because it's already stopped. Listener: "
+                            + listener);
+        }
+    }
+
+    @Override
+    public void resolveHost(String name, INsdResolveHostCallback callback, int listenerId) {
+        mHandler.post(() -> resolveHostInternal(name, callback, listenerId));
+    }
+
+    private void resolveHostInternal(
+            String name, INsdResolveHostCallback callback, int listenerId) {
+        checkOnHandlerThread();
+
+        String fullHostname = name + ".local";
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        HostInfoListener listener =
+                new HostInfoListener(name, callback, cancellationSignal, listenerId);
+        mDnsResolver.query(
+                mNetwork,
+                fullHostname,
+                DnsResolver.FLAG_NO_CACHE_LOOKUP,
+                mExecutor,
+                cancellationSignal,
+                listener);
+        mHostInfoListeners.append(listenerId, listener);
+
+        Log.i(TAG, "Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
+    }
+
+    @Override
+    public void stopHostResolution(int listenerId) {
+        mHandler.post(() -> stopHostResolutionInternal(listenerId));
+    }
+
+    private void stopHostResolutionInternal(int listenerId) {
+        checkOnHandlerThread();
+
+        HostInfoListener listener = mHostInfoListeners.get(listenerId);
+        if (listener == null) {
+            Log.w(
+                    TAG,
+                    "Failed to stop host resolution. Listener ID: "
+                            + listenerId
+                            + ". The listener is null.");
+            return;
+        }
+        Log.i(TAG, "Stopping host resolution. Listener: " + listener);
+        listener.cancel();
+        mHostInfoListeners.remove(listenerId);
+    }
+
     private void checkOnHandlerThread() {
         if (mHandler.getLooper().getThread() != Thread.currentThread()) {
             throw new IllegalStateException(
@@ -178,8 +362,12 @@
         }
     }
 
-    /** On ot-daemon died, unregister all registrations. */
-    public void onOtDaemonDied() {
+    @Override
+    public void reset() {
+        mHandler.post(this::resetInternal);
+    }
+
+    private void resetInternal() {
         checkOnHandlerThread();
         for (int i = 0; i < mRegistrationListeners.size(); ++i) {
             try {
@@ -198,37 +386,9 @@
         mRegistrationListeners.clear();
     }
 
-    // TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
-    /** Fetch the first job from the queue and run it. See the class doc for more details. */
-    private void peekAndRun() {
-        if (mRegistrationJobs.isEmpty()) {
-            return;
-        }
-        Runnable job = mRegistrationJobs.getFirst();
-        job.run();
-    }
-
-    // TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
-    /**
-     * Pop the first job from the queue and run the next job. See the class doc for more details.
-     */
-    private void popAndRunNext() {
-        if (mRegistrationJobs.isEmpty()) {
-            Log.i(TAG, "No registration jobs when trying to pop and run next.");
-            return;
-        }
-        mRegistrationJobs.removeFirst();
-        peekAndRun();
-    }
-
-    private void postRegistrationJob(Runnable registrationJob) {
-        mHandler.post(
-                () -> {
-                    mRegistrationJobs.addLast(registrationJob);
-                    if (mRegistrationJobs.size() == 1) {
-                        peekAndRun();
-                    }
-                });
+    /** On ot-daemon died, reset. */
+    public void onOtDaemonDied() {
+        reset();
     }
 
     private final class RegistrationListener implements NsdManager.RegistrationListener {
@@ -268,7 +428,6 @@
             } catch (RemoteException ignored) {
                 // do nothing if the client is dead
             }
-            popAndRunNext();
         }
 
         @Override
@@ -290,7 +449,6 @@
                     // do nothing if the client is dead
                 }
             }
-            popAndRunNext();
         }
 
         @Override
@@ -308,7 +466,6 @@
             } catch (RemoteException ignored) {
                 // do nothing if the client is dead
             }
-            popAndRunNext();
         }
 
         @Override
@@ -329,7 +486,241 @@
                 }
             }
             mRegistrationListeners.remove(mListenerId);
-            popAndRunNext();
+        }
+    }
+
+    private final class DiscoveryListener implements NsdManager.DiscoveryListener {
+        private final int mListenerId;
+        private final String mType;
+        private final INsdDiscoverServiceCallback mDiscoverServiceCallback;
+
+        DiscoveryListener(
+                int listenerId,
+                @NonNull String type,
+                @NonNull INsdDiscoverServiceCallback discoverServiceCallback) {
+            mListenerId = listenerId;
+            mType = type;
+            mDiscoverServiceCallback = discoverServiceCallback;
+        }
+
+        @Override
+        public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+            Log.e(
+                    TAG,
+                    "Failed to start service discovery."
+                            + " Error code: "
+                            + errorCode
+                            + ", listener: "
+                            + this);
+            mDiscoveryListeners.remove(mListenerId);
+        }
+
+        @Override
+        public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+            Log.e(
+                    TAG,
+                    "Failed to stop service discovery."
+                            + " Error code: "
+                            + errorCode
+                            + ", listener: "
+                            + this);
+            mDiscoveryListeners.remove(mListenerId);
+        }
+
+        @Override
+        public void onDiscoveryStarted(String serviceType) {
+            Log.i(TAG, "Started service discovery. Listener: " + this);
+        }
+
+        @Override
+        public void onDiscoveryStopped(String serviceType) {
+            Log.i(TAG, "Stopped service discovery. Listener: " + this);
+            mDiscoveryListeners.remove(mListenerId);
+        }
+
+        @Override
+        public void onServiceFound(NsdServiceInfo serviceInfo) {
+            Log.i(TAG, "Found service: " + serviceInfo);
+            try {
+                mDiscoverServiceCallback.onServiceDiscovered(
+                        serviceInfo.getServiceName(), mType, true);
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+        }
+
+        @Override
+        public void onServiceLost(NsdServiceInfo serviceInfo) {
+            Log.i(TAG, "Lost service: " + serviceInfo);
+            try {
+                mDiscoverServiceCallback.onServiceDiscovered(
+                        serviceInfo.getServiceName(), mType, false);
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "ID: " + mListenerId + ", type: " + mType;
+        }
+    }
+
+    private final class ServiceInfoListener implements NsdManager.ServiceInfoCallback {
+        private final String mName;
+        private final String mType;
+        private final INsdResolveServiceCallback mResolveServiceCallback;
+        private final int mListenerId;
+
+        ServiceInfoListener(
+                @NonNull NsdServiceInfo serviceInfo,
+                int listenerId,
+                @NonNull INsdResolveServiceCallback resolveServiceCallback) {
+            mName = serviceInfo.getServiceName();
+            mType = serviceInfo.getServiceType();
+            mListenerId = listenerId;
+            mResolveServiceCallback = resolveServiceCallback;
+        }
+
+        @Override
+        public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
+            Log.e(
+                    TAG,
+                    "Failed to register service info callback."
+                            + " Listener ID: "
+                            + mListenerId
+                            + ", error: "
+                            + errorCode
+                            + ", service name: "
+                            + mName
+                            + ", service type: "
+                            + mType);
+        }
+
+        @Override
+        public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+            Log.i(
+                    TAG,
+                    "Service is resolved. "
+                            + " Listener ID: "
+                            + mListenerId
+                            + ", serviceInfo: "
+                            + serviceInfo);
+            List<String> addresses = new ArrayList<>();
+            for (InetAddress address : serviceInfo.getHostAddresses()) {
+                if (address instanceof Inet6Address) {
+                    addresses.add(address.getHostAddress());
+                }
+            }
+            List<DnsTxtAttribute> txtList = new ArrayList<>();
+            for (Map.Entry<String, byte[]> entry : serviceInfo.getAttributes().entrySet()) {
+                DnsTxtAttribute attribute =
+                        new DnsTxtAttribute(entry.getKey(), entry.getValue().clone());
+                txtList.add(attribute);
+            }
+            // TODO: b/329018320 - Use the serviceInfo.getExpirationTime to derive TTL.
+            int ttlSeconds = 10;
+            try {
+                mResolveServiceCallback.onServiceResolved(
+                        serviceInfo.getHostname(),
+                        serviceInfo.getServiceName(),
+                        serviceInfo.getServiceType(),
+                        serviceInfo.getPort(),
+                        addresses,
+                        txtList,
+                        ttlSeconds);
+
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+        }
+
+        @Override
+        public void onServiceLost() {}
+
+        @Override
+        public void onServiceInfoCallbackUnregistered() {
+            Log.i(TAG, "The service info callback is unregistered. Listener: " + this);
+            mServiceInfoListeners.remove(mListenerId);
+        }
+
+        @Override
+        public String toString() {
+            return "ID: " + mListenerId + ", service name: " + mName + ", service type: " + mType;
+        }
+    }
+
+    class HostInfoListener implements DnsResolver.Callback<List<InetAddress>> {
+        private final String mHostname;
+        private final INsdResolveHostCallback mResolveHostCallback;
+        private final CancellationSignal mCancellationSignal;
+        private final int mListenerId;
+
+        HostInfoListener(
+                @NonNull String hostname,
+                INsdResolveHostCallback resolveHostCallback,
+                CancellationSignal cancellationSignal,
+                int listenerId) {
+            this.mHostname = hostname;
+            this.mResolveHostCallback = resolveHostCallback;
+            this.mCancellationSignal = cancellationSignal;
+            this.mListenerId = listenerId;
+        }
+
+        @Override
+        public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
+            checkOnHandlerThread();
+
+            Log.i(
+                    TAG,
+                    "Host is resolved."
+                            + " Listener ID: "
+                            + mListenerId
+                            + ", hostname: "
+                            + mHostname
+                            + ", addresses: "
+                            + answerList
+                            + ", return code: "
+                            + rcode);
+            List<String> addressStrings = new ArrayList<>();
+            for (InetAddress address : answerList) {
+                addressStrings.add(address.getHostAddress());
+            }
+            try {
+                mResolveHostCallback.onHostResolved(mHostname, addressStrings);
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+            mHostInfoListeners.remove(mListenerId);
+        }
+
+        @Override
+        public void onError(@NonNull DnsResolver.DnsException error) {
+            checkOnHandlerThread();
+
+            Log.i(
+                    TAG,
+                    "Failed to resolve host."
+                            + " Listener ID: "
+                            + mListenerId
+                            + ", hostname: "
+                            + mHostname,
+                    error);
+            try {
+                mResolveHostCallback.onHostResolved(mHostname, Collections.emptyList());
+            } catch (RemoteException e) {
+                // do nothing if the client is dead
+            }
+            mHostInfoListeners.remove(mListenerId);
+        }
+
+        public String toString() {
+            return "ID: " + mListenerId + ", hostname: " + mHostname;
+        }
+
+        void cancel() {
+            mCancellationSignal.cancel();
+            mHostInfoListeners.remove(mListenerId);
         }
     }
 }
diff --git a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
index a8909bc..bad63f3 100644
--- a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
+++ b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
@@ -16,9 +16,11 @@
 
 package com.android.server.thread;
 
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
 import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
 
 import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadNetworkException;
 import android.os.RemoteException;
 
 import com.android.internal.annotations.GuardedBy;
@@ -29,6 +31,7 @@
 /** A {@link IOperationReceiver} wrapper which makes it easier to invoke the callbacks. */
 final class OperationReceiverWrapper {
     private final IOperationReceiver mReceiver;
+    private final boolean mExpectOtDaemonDied;
 
     private static final Object sPendingReceiversLock = new Object();
 
@@ -36,7 +39,19 @@
     private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
 
     public OperationReceiverWrapper(IOperationReceiver receiver) {
-        this.mReceiver = receiver;
+        this(receiver, false /* expectOtDaemonDied */);
+    }
+
+    /**
+     * Creates a new {@link OperationReceiverWrapper}.
+     *
+     * <p>If {@code expectOtDaemonDied} is {@code true}, it's expected that ot-daemon becomes dead
+     * before {@code receiver} is completed with {@code onSuccess} and {@code onError} and {@code
+     * receiver#onSuccess} will be invoked in this case.
+     */
+    public OperationReceiverWrapper(IOperationReceiver receiver, boolean expectOtDaemonDied) {
+        mReceiver = receiver;
+        mExpectOtDaemonDied = expectOtDaemonDied;
 
         synchronized (sPendingReceiversLock) {
             sPendingReceivers.add(this);
@@ -47,7 +62,11 @@
         synchronized (sPendingReceiversLock) {
             for (OperationReceiverWrapper receiver : sPendingReceivers) {
                 try {
-                    receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+                    if (receiver.mExpectOtDaemonDied) {
+                        receiver.mReceiver.onSuccess();
+                    } else {
+                        receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+                    }
                 } catch (RemoteException e) {
                     // The client is dead, do nothing
                 }
@@ -68,6 +87,17 @@
         }
     }
 
+    public void onError(Throwable e) {
+        if (e instanceof ThreadNetworkException) {
+            ThreadNetworkException threadException = (ThreadNetworkException) e;
+            onError(threadException.getErrorCode(), threadException.getMessage());
+        } else if (e instanceof RemoteException) {
+            onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        } else {
+            throw new AssertionError(e);
+        }
+    }
+
     public void onError(int errorCode, String errorMessage, Object... messageArgs) {
         synchronized (sPendingReceiversLock) {
             sPendingReceivers.remove(this);
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 0623b87..b621a6a 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -40,6 +40,7 @@
 import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
 import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
 import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
 import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
@@ -47,6 +48,7 @@
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_FAILED_PRECONDITION;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NOT_IMPLEMENTED;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REASSEMBLY_TIMEOUT;
@@ -59,6 +61,8 @@
 import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_ENABLED;
 import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import android.Manifest.permission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -68,9 +72,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.InetAddresses;
-import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.LocalNetworkConfig;
 import android.net.LocalNetworkInfo;
@@ -85,50 +89,64 @@
 import android.net.TestNetworkSpecifier;
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.net.thread.ChannelMaxPower;
 import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IConfigurationReceiver;
 import android.net.thread.IOperationReceiver;
 import android.net.thread.IOperationalDatasetCallback;
 import android.net.thread.IStateCallback;
 import android.net.thread.IThreadNetworkController;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
 import android.net.thread.ThreadNetworkController;
 import android.net.thread.ThreadNetworkController.DeviceRole;
+import android.net.thread.ThreadNetworkException;
 import android.net.thread.ThreadNetworkException.ErrorCode;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserManager;
 import android.util.Log;
 import android.util.SparseArray;
 
+import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.ServiceManagerWrapper;
+import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.thread.openthread.BackboneRouterState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.BorderRouterConfiguration;
+import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.IChannelMasksReceiver;
 import com.android.server.thread.openthread.IOtDaemon;
 import com.android.server.thread.openthread.IOtDaemonCallback;
 import com.android.server.thread.openthread.IOtStatusReceiver;
 import com.android.server.thread.openthread.Ipv6AddressInfo;
+import com.android.server.thread.openthread.MeshcopTxtAttributes;
+import com.android.server.thread.openthread.OnMeshPrefixConfig;
 import com.android.server.thread.openthread.OtDaemonState;
 
+import libcore.util.HexEncoding;
+
 import java.io.IOException;
 import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.security.SecureRandom;
+import java.time.Clock;
+import java.time.DateTimeException;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Random;
 import java.util.function.Supplier;
+import java.util.regex.Pattern;
 
 /**
  * Implementation of the {@link ThreadNetworkController} API.
@@ -143,6 +161,19 @@
 final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
     private static final String TAG = "ThreadNetworkService";
 
+    // The model name length in utf-8 bytes
+    private static final int MAX_MODEL_NAME_UTF8_BYTES = 24;
+
+    // The max vendor name length in utf-8 bytes
+    private static final int MAX_VENDOR_NAME_UTF8_BYTES = 24;
+
+    // This regex pattern allows "XXXXXX", "XX:XX:XX" and "XX-XX-XX" OUI formats.
+    // Note that this regex allows "XX:XX-XX" as well but we don't need to be a strict checker
+    private static final String OUI_REGEX = "^([0-9A-Fa-f]{2}[:-]?){2}([0-9A-Fa-f]{2})$";
+
+    // The channel mask that indicates all channels from channel 11 to channel 24
+    private static final int CHANNEL_MASK_11_TO_24 = 0x1FFF800;
+
     // Below member fields can be accessed from both the binder and handler threads
 
     private final Context mContext;
@@ -159,7 +190,13 @@
     private final InfraInterfaceController mInfraIfController;
     private final NsdPublisher mNsdPublisher;
     private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+    private final ConnectivityResources mResources;
+    private final Supplier<String> mCountryCodeSupplier;
+    private final Map<IConfigurationReceiver, IBinder.DeathRecipient> mConfigurationReceivers =
+            new HashMap<>();
 
+    // This should not be directly used for calling IOtDaemon APIs because ot-daemon may die and
+    // {@code mOtDaemon} will be set to {@code null}. Instead, use {@code getOtDaemon()}
     @Nullable private IOtDaemon mOtDaemon;
     @Nullable private NetworkAgent mNetworkAgent;
     @Nullable private NetworkAgent mTestNetworkAgent;
@@ -174,8 +211,9 @@
     private final ThreadPersistentSettings mPersistentSettings;
     private final UserManager mUserManager;
     private boolean mUserRestricted;
+    private boolean mForceStopOtDaemonEnabled;
 
-    private BorderRouterConfigurationParcel mBorderRouterConfig;
+    private BorderRouterConfiguration mBorderRouterConfig;
 
     @VisibleForTesting
     ThreadNetworkControllerService(
@@ -188,7 +226,9 @@
             InfraInterfaceController infraIfController,
             ThreadPersistentSettings persistentSettings,
             NsdPublisher nsdPublisher,
-            UserManager userManager) {
+            UserManager userManager,
+            ConnectivityResources resources,
+            Supplier<String> countryCodeSupplier) {
         mContext = context;
         mHandler = handler;
         mNetworkProvider = networkProvider;
@@ -198,14 +238,22 @@
         mInfraIfController = infraIfController;
         mUpstreamNetworkRequest = newUpstreamNetworkRequest();
         mNetworkToInterface = new HashMap<Network, String>();
-        mBorderRouterConfig = new BorderRouterConfigurationParcel();
+        mBorderRouterConfig =
+                new BorderRouterConfiguration.Builder()
+                        .setIsBorderRoutingEnabled(true)
+                        .setInfraInterfaceName(null)
+                        .build();
         mPersistentSettings = persistentSettings;
         mNsdPublisher = nsdPublisher;
         mUserManager = userManager;
+        mResources = resources;
+        mCountryCodeSupplier = countryCodeSupplier;
     }
 
     public static ThreadNetworkControllerService newInstance(
-            Context context, ThreadPersistentSettings persistentSettings) {
+            Context context,
+            ThreadPersistentSettings persistentSettings,
+            Supplier<String> countryCodeSupplier) {
         HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
         handlerThread.start();
         Handler handler = new Handler(handlerThread.getLooper());
@@ -222,46 +270,18 @@
                 new InfraInterfaceController(),
                 persistentSettings,
                 NsdPublisher.newInstance(context, handler),
-                context.getSystemService(UserManager.class));
-    }
-
-    private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
-        try {
-            return (Inet6Address) Inet6Address.getByAddress(addressBytes);
-        } catch (UnknownHostException e) {
-            // This is unlikely to happen unless the Thread daemon is critically broken
-            return null;
-        }
-    }
-
-    private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
-        return bytesToInet6Address(addressInfo.address);
-    }
-
-    private static LinkAddress newLinkAddress(Ipv6AddressInfo addressInfo) {
-        long deprecationTimeMillis =
-                addressInfo.isPreferred
-                        ? LinkAddress.LIFETIME_PERMANENT
-                        : SystemClock.elapsedRealtime();
-
-        InetAddress address = addressInfoToInetAddress(addressInfo);
-
-        // flags and scope will be adjusted automatically depending on the address and
-        // its lifetimes.
-        return new LinkAddress(
-                address,
-                addressInfo.prefixLength,
-                0 /* flags */,
-                0 /* scope */,
-                deprecationTimeMillis,
-                LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
+                context.getSystemService(UserManager.class),
+                new ConnectivityResources(context),
+                countryCodeSupplier);
     }
 
     private NetworkRequest newUpstreamNetworkRequest() {
-        NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
 
         if (mUpstreamTestNetworkSpecifier != null) {
-            return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            // Test networks don't have NET_CAPABILITY_TRUSTED
+            return builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
                     .setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
                     .build();
         }
@@ -279,17 +299,31 @@
                 .build();
     }
 
-    private void initializeOtDaemon() {
+    private void maybeInitializeOtDaemon() {
+        if (!shouldEnableThread()) {
+            return;
+        }
+
+        Log.i(TAG, "Starting OT daemon...");
+
         try {
             getOtDaemon();
         } catch (RemoteException e) {
-            Log.e(TAG, "Failed to initialize ot-daemon");
+            Log.e(TAG, "Failed to initialize ot-daemon", e);
+        } catch (ThreadNetworkException e) {
+            // no ThreadNetworkException.ERROR_THREAD_DISABLED error should be thrown
+            throw new AssertionError(e);
         }
     }
 
-    private IOtDaemon getOtDaemon() throws RemoteException {
+    private IOtDaemon getOtDaemon() throws RemoteException, ThreadNetworkException {
         checkOnHandlerThread();
 
+        if (mForceStopOtDaemonEnabled) {
+            throw new ThreadNetworkException(
+                    ERROR_THREAD_DISABLED, "ot-daemon is forcibly stopped");
+        }
+
         if (mOtDaemon != null) {
             return mOtDaemon;
         }
@@ -298,29 +332,114 @@
         if (otDaemon == null) {
             throw new RemoteException("Internal error: failed to start OT daemon");
         }
-        otDaemon.initialize(mTunIfController.getTunFd(), isEnabled(), mNsdPublisher);
-        otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+
+        otDaemon.initialize(
+                mTunIfController.getTunFd(),
+                shouldEnableThread(),
+                mNsdPublisher,
+                getMeshcopTxtAttributes(mResources.get()),
+                mOtDaemonCallbackProxy,
+                mCountryCodeSupplier.get());
         otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
         mOtDaemon = otDaemon;
         return mOtDaemon;
     }
 
+    @VisibleForTesting
+    static MeshcopTxtAttributes getMeshcopTxtAttributes(Resources resources) {
+        final String modelName = resources.getString(R.string.config_thread_model_name);
+        final String vendorName = resources.getString(R.string.config_thread_vendor_name);
+        final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
+        final String[] vendorSpecificTxts =
+                resources.getStringArray(R.array.config_thread_mdns_vendor_specific_txts);
+
+        if (!modelName.isEmpty()) {
+            if (modelName.getBytes(UTF_8).length > MAX_MODEL_NAME_UTF8_BYTES) {
+                throw new IllegalStateException(
+                        "Model name is longer than "
+                                + MAX_MODEL_NAME_UTF8_BYTES
+                                + "utf-8 bytes: "
+                                + modelName);
+            }
+        }
+
+        if (!vendorName.isEmpty()) {
+            if (vendorName.getBytes(UTF_8).length > MAX_VENDOR_NAME_UTF8_BYTES) {
+                throw new IllegalStateException(
+                        "Vendor name is longer than "
+                                + MAX_VENDOR_NAME_UTF8_BYTES
+                                + " utf-8 bytes: "
+                                + vendorName);
+            }
+        }
+
+        if (!vendorOui.isEmpty() && !Pattern.compile(OUI_REGEX).matcher(vendorOui).matches()) {
+            throw new IllegalStateException("Vendor OUI is invalid: " + vendorOui);
+        }
+
+        MeshcopTxtAttributes meshcopTxts = new MeshcopTxtAttributes();
+        meshcopTxts.modelName = modelName;
+        meshcopTxts.vendorName = vendorName;
+        meshcopTxts.vendorOui = HexEncoding.decode(vendorOui.replace("-", "").replace(":", ""));
+        meshcopTxts.nonStandardTxtEntries = makeVendorSpecificTxtAttrs(vendorSpecificTxts);
+
+        return meshcopTxts;
+    }
+
+    /**
+     * Parses vendor-specific TXT entries from "=" separated strings into list of {@link
+     * DnsTxtAttribute}.
+     *
+     * @throws IllegalArgumentsException if invalid TXT entries are found in {@code vendorTxts}
+     */
+    @VisibleForTesting
+    static List<DnsTxtAttribute> makeVendorSpecificTxtAttrs(String[] vendorTxts) {
+        List<DnsTxtAttribute> txts = new ArrayList<>();
+        for (String txt : vendorTxts) {
+            String[] kv = txt.split("=", 2 /* limit */); // Split with only the first '='
+            if (kv.length < 1) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT is found in resources: " + txt);
+            }
+
+            if (kv[0].length() < 2) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT key \""
+                                + kv[0]
+                                + "\": it must contain at least 2 characters");
+            }
+
+            if (!kv[0].startsWith("v")) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT key \""
+                                + kv[0]
+                                + "\": it doesn't start with \"v\"");
+            }
+
+            txts.add(new DnsTxtAttribute(kv[0], (kv.length >= 2 ? kv[1] : "").getBytes(UTF_8)));
+        }
+        return txts;
+    }
+
     private void onOtDaemonDied() {
         checkOnHandlerThread();
-        Log.w(TAG, "OT daemon is dead, clean up and restart it...");
+        Log.w(TAG, "OT daemon is dead, clean up...");
 
         OperationReceiverWrapper.onOtDaemonDied();
         mOtDaemonCallbackProxy.onOtDaemonDied();
         mTunIfController.onOtDaemonDied();
         mNsdPublisher.onOtDaemonDied();
         mOtDaemon = null;
-        initializeOtDaemon();
+        maybeInitializeOtDaemon();
     }
 
     public void initialize() {
         mHandler.post(
                 () -> {
-                    Log.d(TAG, "Initializing Thread system service...");
+                    Log.d(
+                            TAG,
+                            "Initializing Thread system service: Thread is "
+                                    + (shouldEnableThread() ? "enabled" : "disabled"));
                     try {
                         mTunIfController.createTunInterface();
                     } catch (IOException e) {
@@ -332,10 +451,59 @@
                     requestThreadNetwork();
                     mUserRestricted = isThreadUserRestricted();
                     registerUserRestrictionsReceiver();
-                    initializeOtDaemon();
+                    maybeInitializeOtDaemon();
                 });
     }
 
+    /**
+     * Force stops ot-daemon immediately and prevents ot-daemon from being restarted by
+     * system_server again.
+     *
+     * <p>This is for VTS testing only.
+     */
+    @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+    void forceStopOtDaemonForTest(boolean enabled, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        mHandler.post(
+                () ->
+                        forceStopOtDaemonForTestInternal(
+                                enabled,
+                                new OperationReceiverWrapper(
+                                        receiver, true /* expectOtDaemonDied */)));
+    }
+
+    private void forceStopOtDaemonForTestInternal(
+            boolean enabled, @NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+        if (enabled == mForceStopOtDaemonEnabled) {
+            receiver.onSuccess();
+            return;
+        }
+
+        if (!enabled) {
+            mForceStopOtDaemonEnabled = false;
+            maybeInitializeOtDaemon();
+            receiver.onSuccess();
+            return;
+        }
+
+        try {
+            getOtDaemon().terminate();
+            // Do not invoke the {@code receiver} callback here but wait for ot-daemon to
+            // become dead, so that it's guaranteed that ot-daemon is stopped when {@code
+            // receiver} is completed
+        } catch (RemoteException e) {
+            Log.e(TAG, "otDaemon.terminate failed", e);
+            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        } catch (ThreadNetworkException e) {
+            // No ThreadNetworkException.ERROR_THREAD_DISABLED error will be thrown
+            throw new AssertionError(e);
+        } finally {
+            mForceStopOtDaemonEnabled = true;
+        }
+    }
+
     public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) {
         enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
 
@@ -349,6 +517,7 @@
 
     private void setEnabledInternal(
             boolean isEnabled, boolean persist, @NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
         if (isEnabled && isThreadUserRestricted()) {
             receiver.onError(
                     ERROR_FAILED_PRECONDITION,
@@ -356,6 +525,8 @@
             return;
         }
 
+        Log.i(TAG, "Set Thread enabled: " + isEnabled + ", persist: " + persist);
+
         if (persist) {
             // The persistent setting keeps the desired enabled state, thus it's set regardless
             // the otDaemon set enabled state operation succeeded or not, so that it can recover
@@ -365,12 +536,83 @@
 
         try {
             getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
-        } catch (RemoteException e) {
+        } catch (RemoteException | ThreadNetworkException e) {
             Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
-            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+            receiver.onError(e);
         }
     }
 
+    @Override
+    public void setConfiguration(
+            @NonNull ThreadConfiguration configuration, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> setConfigurationInternal(configuration, receiver));
+    }
+
+    private void setConfigurationInternal(
+            @NonNull ThreadConfiguration configuration,
+            @NonNull IOperationReceiver operationReceiver) {
+        checkOnHandlerThread();
+
+        Log.i(TAG, "Set Thread configuration: " + configuration);
+
+        final boolean changed = mPersistentSettings.putConfiguration(configuration);
+        try {
+            operationReceiver.onSuccess();
+        } catch (RemoteException e) {
+            // do nothing if the client is dead
+        }
+        if (changed) {
+            for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) {
+                try {
+                    configReceiver.onConfigurationChanged(configuration);
+                } catch (RemoteException e) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+    }
+
+    @Override
+    public void registerConfigurationCallback(@NonNull IConfigurationReceiver callback) {
+        enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> registerConfigurationCallbackInternal(callback));
+    }
+
+    private void registerConfigurationCallbackInternal(@NonNull IConfigurationReceiver callback) {
+        checkOnHandlerThread();
+        if (mConfigurationReceivers.containsKey(callback)) {
+            throw new IllegalStateException("Registering the same IConfigurationReceiver twice");
+        }
+        IBinder.DeathRecipient deathRecipient =
+                () -> mHandler.post(() -> unregisterConfigurationCallbackInternal(callback));
+        try {
+            callback.asBinder().linkToDeath(deathRecipient, 0);
+        } catch (RemoteException e) {
+            return;
+        }
+        mConfigurationReceivers.put(callback, deathRecipient);
+        try {
+            callback.onConfigurationChanged(mPersistentSettings.getConfiguration());
+        } catch (RemoteException e) {
+            // do nothing if the client is dead
+        }
+    }
+
+    @Override
+    public void unregisterConfigurationCallback(@NonNull IConfigurationReceiver callback) {
+        enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> unregisterConfigurationCallbackInternal(callback));
+    }
+
+    private void unregisterConfigurationCallbackInternal(@NonNull IConfigurationReceiver callback) {
+        checkOnHandlerThread();
+        if (!mConfigurationReceivers.containsKey(callback)) {
+            return;
+        }
+        callback.asBinder().unlinkToDeath(mConfigurationReceivers.remove(callback), 0);
+    }
+
     private void registerUserRestrictionsReceiver() {
         mContext.registerReceiver(
                 new BroadcastReceiver() {
@@ -397,34 +639,30 @@
                         + newUserRestrictedState);
         mUserRestricted = newUserRestrictedState;
 
-        final boolean isEnabled = isEnabled();
+        final boolean shouldEnableThread = shouldEnableThread();
         final IOperationReceiver receiver =
                 new IOperationReceiver.Stub() {
                     @Override
                     public void onSuccess() {
                         Log.d(
                                 TAG,
-                                (isEnabled ? "Enabled" : "Disabled")
+                                (shouldEnableThread ? "Enabled" : "Disabled")
                                         + " Thread due to user restriction change");
                     }
 
                     @Override
-                    public void onError(int otError, String messages) {
+                    public void onError(int errorCode, String errorMessage) {
                         Log.e(
                                 TAG,
                                 "Failed to "
-                                        + (isEnabled ? "enable" : "disable")
+                                        + (shouldEnableThread ? "enable" : "disable")
                                         + " Thread for user restriction change");
                     }
                 };
         // Do not save the user restriction state to persistent settings so that the user
         // configuration won't be overwritten
-        setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
-    }
-
-    /** Returns {@code true} if Thread is set enabled. */
-    private boolean isEnabled() {
-        return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+        setEnabledInternal(
+                shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
     }
 
     /** Returns {@code true} if Thread has been restricted for the user. */
@@ -432,6 +670,16 @@
         return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
     }
 
+    /**
+     * Returns {@code true} if Thread should be enabled based on current settings, runtime user
+     * restriction state.
+     */
+    private boolean shouldEnableThread() {
+        return !mForceStopOtDaemonEnabled
+                && !mUserRestricted
+                && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+    }
+
     private void requestUpstreamNetwork() {
         if (mUpstreamNetworkCallback != null) {
             throw new AssertionError("The upstream network request is already there.");
@@ -489,7 +737,14 @@
         @Override
         public void onAvailable(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "Thread network available: " + network);
+            Log.i(TAG, "Thread network is available: " + network);
+        }
+
+        @Override
+        public void onLost(@NonNull Network network) {
+            checkOnHandlerThread();
+            Log.i(TAG, "Thread network is lost: " + network);
+            disableBorderRouting();
         }
 
         @Override
@@ -504,7 +759,7 @@
                             + localNetworkInfo
                             + "}");
             if (localNetworkInfo.getUpstreamNetwork() == null) {
-                mUpstreamNetwork = null;
+                disableBorderRouting();
                 return;
             }
             if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
@@ -512,6 +767,7 @@
                 if (mNetworkToInterface.containsKey(mUpstreamNetwork)) {
                     enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
                 }
+                mNsdPublisher.setNetworkForHostResolution(mUpstreamNetwork);
             }
         }
     }
@@ -523,6 +779,7 @@
                         // requirement.
                         .clearCapabilities()
                         .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
                         .build(),
                 new ThreadNetworkCallback(),
                 mHandler);
@@ -543,6 +800,7 @@
                 new NetworkCapabilities.Builder()
                         .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
                         .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
                         .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
                         .build();
         final NetworkScore score =
@@ -604,9 +862,9 @@
 
         try {
             getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
-        } catch (RemoteException e) {
+        } catch (RemoteException | ThreadNetworkException e) {
             Log.e(TAG, "otDaemon.getChannelMasks failed", e);
-            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+            receiver.onError(e);
         }
     }
 
@@ -620,7 +878,6 @@
                                 networkName,
                                 supportedChannelMask,
                                 preferredChannelMask,
-                                Instant.now(),
                                 new Random(),
                                 new SecureRandom());
 
@@ -638,9 +895,18 @@
             String networkName,
             int supportedChannelMask,
             int preferredChannelMask,
-            Instant now,
             Random random,
             SecureRandom secureRandom) {
+        boolean authoritative = false;
+        Instant now = Instant.now();
+        try {
+            Clock clock = SystemClock.currentNetworkTimeClock();
+            now = clock.instant();
+            authoritative = true;
+        } catch (DateTimeException e) {
+            Log.w(TAG, "Failed to get authoritative time", e);
+        }
+
         int panId = random.nextInt(/* bound= */ 0xffff);
         final byte[] meshLocalPrefix = newRandomBytes(random, LENGTH_MESH_LOCAL_PREFIX_BITS / 8);
         meshLocalPrefix[0] = MESH_LOCAL_PREFIX_FIRST_BYTE;
@@ -652,9 +918,7 @@
         final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
 
         return new ActiveOperationalDataset.Builder()
-                .setActiveTimestamp(
-                        new OperationalDatasetTimestamp(
-                                now.getEpochSecond() & 0xffffffffffffL, 0, false))
+                .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(now, authoritative))
                 .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
                 .setPanId(panId)
                 .setNetworkName(networkName)
@@ -669,6 +933,14 @@
 
     private static int selectChannel(
             int supportedChannelMask, int preferredChannelMask, Random random) {
+        // Due to radio hardware performance reasons, many Thread radio chips need to reduce their
+        // transmit power on edge channels to pass regulatory RF certification. Thread edge channel
+        // 25 and 26 are not preferred here.
+        //
+        // If users want to use channel 25 or 26, they can change the channel via the method
+        // ActiveOperationalDataset.Builder(activeOperationalDataset).setChannel(channel).build().
+        preferredChannelMask = preferredChannelMask & CHANNEL_MASK_11_TO_24;
+
         // If the preferred channel mask is not empty, select a random channel from it, otherwise
         // choose one from the supported channel mask.
         preferredChannelMask = preferredChannelMask & supportedChannelMask;
@@ -752,7 +1024,11 @@
 
     private void checkOnHandlerThread() {
         if (Looper.myLooper() != mHandler.getLooper()) {
-            Log.wtf(TAG, "Must be on the handler thread!");
+            throw new IllegalStateException(
+                    "Not running on ThreadNetworkControllerService thread ("
+                            + mHandler.getLooper()
+                            + ") : "
+                            + Looper.myLooper());
         }
     }
 
@@ -778,6 +1054,8 @@
                 return ERROR_ABORTED;
             case OT_ERROR_BUSY:
                 return ERROR_BUSY;
+            case OT_ERROR_NOT_IMPLEMENTED:
+                return ERROR_UNSUPPORTED_OPERATION;
             case OT_ERROR_NO_BUFS:
                 return ERROR_RESOURCE_EXHAUSTED;
             case OT_ERROR_PARSE:
@@ -816,9 +1094,9 @@
         try {
             // The otDaemon.join() will leave first if this device is currently attached
             getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
-        } catch (RemoteException e) {
+        } catch (RemoteException | ThreadNetworkException e) {
             Log.e(TAG, "otDaemon.join failed", e);
-            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+            receiver.onError(e);
         }
     }
 
@@ -841,14 +1119,14 @@
             getOtDaemon()
                     .scheduleMigration(
                             pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
-        } catch (RemoteException e) {
+        } catch (RemoteException | ThreadNetworkException e) {
             Log.e(TAG, "otDaemon.scheduleMigration failed", e);
-            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+            receiver.onError(e);
         }
     }
 
     @Override
-    public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
+    public void leave(@NonNull IOperationReceiver receiver) {
         enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
 
         mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
@@ -859,9 +1137,9 @@
 
         try {
             getOtDaemon().leave(newOtStatusReceiver(receiver));
-        } catch (RemoteException e) {
-            // Oneway AIDL API should never throw?
-            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.e(TAG, "otDaemon.leave failed", e);
+            receiver.onError(e);
         }
     }
 
@@ -883,11 +1161,18 @@
             String countryCode, @NonNull OperationReceiverWrapper receiver) {
         checkOnHandlerThread();
 
+        // Fails early to avoid waking up ot-daemon by the ThreadNetworkCountryCode class
+        if (!shouldEnableThread()) {
+            receiver.onError(
+                    ERROR_THREAD_DISABLED, "Can't set country code when Thread is disabled");
+            return;
+        }
+
         try {
             getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
-        } catch (RemoteException e) {
+        } catch (RemoteException | ThreadNetworkException e) {
             Log.e(TAG, "otDaemon.setCountryCode failed", e);
-            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+            receiver.onError(e);
         }
     }
 
@@ -923,37 +1208,78 @@
         }
     }
 
-    private void enableBorderRouting(String infraIfName) {
-        if (mBorderRouterConfig.isBorderRoutingEnabled
-                && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
+    @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+    public void setChannelMaxPowers(
+            @NonNull ChannelMaxPower[] channelMaxPowers, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        mHandler.post(
+                () ->
+                        setChannelMaxPowersInternal(
+                                channelMaxPowers, new OperationReceiverWrapper(receiver)));
+    }
+
+    private void setChannelMaxPowersInternal(
+            @NonNull ChannelMaxPower[] channelMaxPowers,
+            @NonNull OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon().setChannelMaxPowers(channelMaxPowers, newOtStatusReceiver(receiver));
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.e(TAG, "otDaemon.setChannelMaxPowers failed", e);
+            receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+        }
+    }
+
+    private void configureBorderRouter(BorderRouterConfiguration borderRouterConfig) {
+        if (mBorderRouterConfig.equals(borderRouterConfig)) {
             return;
         }
-        Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
-        try {
-            mBorderRouterConfig.infraInterfaceName = infraIfName;
-            mBorderRouterConfig.infraInterfaceIcmp6Socket =
-                    mInfraIfController.createIcmp6Socket(infraIfName);
-            mBorderRouterConfig.isBorderRoutingEnabled = true;
-
-            mOtDaemon.configureBorderRouter(
-                    mBorderRouterConfig,
-                    new IOtStatusReceiver.Stub() {
-                        @Override
-                        public void onSuccess() {
-                            Log.i(TAG, "configure border router successfully");
-                        }
-
-                        @Override
-                        public void onError(int i, String s) {
-                            Log.w(
-                                    TAG,
-                                    String.format(
-                                            "failed to configure border router: %d %s", i, s));
-                        }
-                    });
-        } catch (Exception e) {
-            Log.w(TAG, "enableBorderRouting failed: " + e);
+        Log.i(
+                TAG,
+                "Configuring Border Router: " + mBorderRouterConfig + " -> " + borderRouterConfig);
+        mBorderRouterConfig = borderRouterConfig;
+        ParcelFileDescriptor infraIcmp6Socket = null;
+        if (mBorderRouterConfig.infraInterfaceName != null) {
+            try {
+                infraIcmp6Socket =
+                        mInfraIfController.createIcmp6Socket(
+                                mBorderRouterConfig.infraInterfaceName);
+            } catch (IOException e) {
+                Log.i(TAG, "Failed to create ICMPv6 socket on infra network interface", e);
+            }
         }
+        try {
+            getOtDaemon()
+                    .configureBorderRouter(
+                            mBorderRouterConfig,
+                            infraIcmp6Socket,
+                            new ConfigureBorderRouterStatusReceiver());
+        } catch (RemoteException | ThreadNetworkException e) {
+            Log.w(TAG, "Failed to configure border router " + mBorderRouterConfig, e);
+        }
+    }
+
+    private void enableBorderRouting(String infraIfName) {
+        BorderRouterConfiguration borderRouterConfig =
+                newBorderRouterConfigBuilder(mBorderRouterConfig)
+                        .setIsBorderRoutingEnabled(true)
+                        .setInfraInterfaceName(infraIfName)
+                        .build();
+        Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
+        configureBorderRouter(borderRouterConfig);
+    }
+
+    private void disableBorderRouting() {
+        mUpstreamNetwork = null;
+        BorderRouterConfiguration borderRouterConfig =
+                newBorderRouterConfigBuilder(mBorderRouterConfig)
+                        .setIsBorderRoutingEnabled(false)
+                        .setInfraInterfaceName(null)
+                        .build();
+        Log.i(TAG, "Disabling border routing");
+        configureBorderRouter(borderRouterConfig);
     }
 
     private void handleThreadInterfaceStateChanged(boolean isUp) {
@@ -981,20 +1307,22 @@
         }
     }
 
-    private void handleAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
+    private void handleAddressChanged(List<Ipv6AddressInfo> addressInfoList) {
         checkOnHandlerThread();
-        InetAddress address = addressInfoToInetAddress(addressInfo);
-        if (address.isMulticastAddress()) {
-            Log.i(TAG, "Ignoring multicast address " + address.getHostAddress());
-            return;
-        }
 
-        LinkAddress linkAddress = newLinkAddress(addressInfo);
-        if (isAdded) {
-            mTunIfController.addAddress(linkAddress);
-        } else {
-            mTunIfController.removeAddress(linkAddress);
+        mTunIfController.updateAddresses(addressInfoList);
+
+        // The OT daemon can send link property updates before the networkAgent is
+        // registered
+        if (mNetworkAgent != null) {
+            mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
         }
+    }
+
+    private void handlePrefixChanged(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
+        checkOnHandlerThread();
+
+        mTunIfController.updatePrefixes(onMeshPrefixConfigList);
 
         // The OT daemon can send link property updates before the networkAgent is
         // registered
@@ -1052,6 +1380,13 @@
         return builder.build();
     }
 
+    private static BorderRouterConfiguration.Builder newBorderRouterConfigBuilder(
+            BorderRouterConfiguration brConfig) {
+        return new BorderRouterConfiguration.Builder()
+                .setIsBorderRoutingEnabled(brConfig.isBorderRoutingEnabled)
+                .setInfraInterfaceName(brConfig.infraInterfaceName);
+    }
+
     private static final class CallbackMetadata {
         private static long gId = 0;
 
@@ -1073,6 +1408,20 @@
         }
     }
 
+    private static final class ConfigureBorderRouterStatusReceiver extends IOtStatusReceiver.Stub {
+        public ConfigureBorderRouterStatusReceiver() {}
+
+        @Override
+        public void onSuccess() {
+            Log.i(TAG, "Configured border router successfully");
+        }
+
+        @Override
+        public void onError(int i, String s) {
+            Log.w(TAG, String.format("Failed to configure border router: %d %s", i, s));
+        }
+    }
+
     /**
      * Handles and forwards Thread daemon callbacks. This class must be accessed from the thread of
      * {@code mHandler}.
@@ -1105,17 +1454,8 @@
 
             try {
                 getOtDaemon().registerStateCallback(this, callbackMetadata.id);
-            } catch (RemoteException e) {
-                // oneway operation should never fail
-            }
-        }
-
-        private void notifyThreadEnabledUpdated(IStateCallback callback, int enabledState) {
-            try {
-                callback.onThreadEnableStateChanged(enabledState);
-                Log.i(TAG, "onThreadEnableStateChanged " + enabledState);
-            } catch (RemoteException ignored) {
-                // do nothing if the client is dead
+            } catch (RemoteException | ThreadNetworkException e) {
+                Log.e(TAG, "otDaemon.registerStateCallback failed", e);
             }
         }
 
@@ -1146,8 +1486,8 @@
 
             try {
                 getOtDaemon().registerStateCallback(this, callbackMetadata.id);
-            } catch (RemoteException e) {
-                // oneway operation should never fail
+            } catch (RemoteException | ThreadNetworkException e) {
+                Log.e(TAG, "otDaemon.registerStateCallback failed", e);
             }
         }
 
@@ -1166,8 +1506,11 @@
                 return;
             }
 
+            final int deviceRole = mState.deviceRole;
+            mState = null;
+
             // If this device is already STOPPED or DETACHED, do nothing
-            if (!ThreadNetworkController.isAttached(mState.deviceRole)) {
+            if (!ThreadNetworkController.isAttached(deviceRole)) {
                 return;
             }
 
@@ -1182,15 +1525,19 @@
             }
         }
 
-        @Override
-        public void onThreadEnabledChanged(int state) {
-            mHandler.post(() -> onThreadEnabledChangedInternal(state));
-        }
-
-        private void onThreadEnabledChangedInternal(int state) {
+        private void onThreadEnabledChanged(int state, long listenerId) {
             checkOnHandlerThread();
-            for (IStateCallback callback : mStateCallbacks.keySet()) {
-                notifyThreadEnabledUpdated(callback, otStateToAndroidState(state));
+            boolean stateChanged = (mState == null || mState.threadEnabled != state);
+
+            for (var callbackEntry : mStateCallbacks.entrySet()) {
+                if (!stateChanged && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                try {
+                    callbackEntry.getKey().onThreadEnableStateChanged(otStateToAndroidState(state));
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
             }
         }
 
@@ -1217,6 +1564,7 @@
             onInterfaceStateChanged(newState.isInterfaceUp);
             onDeviceRoleChanged(newState.deviceRole, listenerId);
             onPartitionIdChanged(newState.partitionId, listenerId);
+            onThreadEnabledChanged(newState.threadEnabled, listenerId);
             mState = newState;
 
             ActiveOperationalDataset newActiveDataset;
@@ -1326,13 +1674,18 @@
         }
 
         @Override
-        public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
-            mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
+        public void onAddressChanged(List<Ipv6AddressInfo> addressInfoList) {
+            mHandler.post(() -> handleAddressChanged(addressInfoList));
         }
 
         @Override
         public void onBackboneRouterStateChanged(BackboneRouterState state) {
             mHandler.post(() -> handleMulticastForwardingChanged(state));
         }
+
+        @Override
+        public void onPrefixChanged(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
+            mHandler.post(() -> handlePrefixChanged(onMeshPrefixConfigList));
+        }
     }
 }
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
index ffa7b44..a194114 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -16,6 +16,8 @@
 
 package com.android.server.thread;
 
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
+
 import android.annotation.Nullable;
 import android.annotation.StringDef;
 import android.annotation.TargetApi;
@@ -83,6 +85,7 @@
                 COUNTRY_CODE_SOURCE_TELEPHONY,
                 COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
                 COUNTRY_CODE_SOURCE_WIFI,
+                COUNTRY_CODE_SOURCE_SETTINGS,
             })
     private @interface CountryCodeSource {}
 
@@ -93,6 +96,7 @@
     private static final String COUNTRY_CODE_SOURCE_TELEPHONY = "Telephony";
     private static final String COUNTRY_CODE_SOURCE_TELEPHONY_LAST = "TelephonyLast";
     private static final String COUNTRY_CODE_SOURCE_WIFI = "Wifi";
+    private static final String COUNTRY_CODE_SOURCE_SETTINGS = "Settings";
 
     private static final CountryCodeInfo DEFAULT_COUNTRY_CODE_INFO =
             new CountryCodeInfo(DEFAULT_COUNTRY_CODE, COUNTRY_CODE_SOURCE_DEFAULT);
@@ -107,6 +111,7 @@
     private final SubscriptionManager mSubscriptionManager;
     private final Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeSlotInfoMap =
             new ArrayMap();
+    private final ThreadPersistentSettings mPersistentSettings;
 
     @Nullable private CountryCodeInfo mCurrentCountryCodeInfo;
     @Nullable private CountryCodeInfo mLocationCountryCodeInfo;
@@ -215,7 +220,8 @@
             Context context,
             TelephonyManager telephonyManager,
             SubscriptionManager subscriptionManager,
-            @Nullable String oemCountryCode) {
+            @Nullable String oemCountryCode,
+            ThreadPersistentSettings persistentSettings) {
         mLocationManager = locationManager;
         mThreadNetworkControllerService = threadNetworkControllerService;
         mGeocoder = geocoder;
@@ -224,14 +230,19 @@
         mContext = context;
         mTelephonyManager = telephonyManager;
         mSubscriptionManager = subscriptionManager;
+        mPersistentSettings = persistentSettings;
 
         if (oemCountryCode != null) {
             mOemCountryCodeInfo = new CountryCodeInfo(oemCountryCode, COUNTRY_CODE_SOURCE_OEM);
         }
+
+        mCurrentCountryCodeInfo = pickCountryCode();
     }
 
     public static ThreadNetworkCountryCode newInstance(
-            Context context, ThreadNetworkControllerService controllerService) {
+            Context context,
+            ThreadNetworkControllerService controllerService,
+            ThreadPersistentSettings persistentSettings) {
         return new ThreadNetworkCountryCode(
                 context.getSystemService(LocationManager.class),
                 controllerService,
@@ -241,7 +252,8 @@
                 context,
                 context.getSystemService(TelephonyManager.class),
                 context.getSystemService(SubscriptionManager.class),
-                ThreadNetworkProperties.country_code().orElse(null));
+                ThreadNetworkProperties.country_code().orElse(null),
+                persistentSettings);
     }
 
     /** Sets up this country code module to listen to location country code changes. */
@@ -485,6 +497,11 @@
             return mLocationCountryCodeInfo;
         }
 
+        String settingsCountryCode = mPersistentSettings.get(THREAD_COUNTRY_CODE);
+        if (settingsCountryCode != null) {
+            return new CountryCodeInfo(settingsCountryCode, COUNTRY_CODE_SOURCE_SETTINGS);
+        }
+
         if (mOemCountryCodeInfo != null) {
             return mOemCountryCodeInfo;
         }
@@ -498,6 +515,8 @@
             public void onSuccess() {
                 synchronized ("ThreadNetworkCountryCode.this") {
                     mCurrentCountryCodeInfo = countryCodeInfo;
+                    mPersistentSettings.put(
+                            THREAD_COUNTRY_CODE.key, countryCodeInfo.getCountryCode());
                 }
             }
 
@@ -536,10 +555,9 @@
                 newOperationReceiver(countryCodeInfo));
     }
 
-    /** Returns the current country code or {@code null} if no country code is set. */
-    @Nullable
+    /** Returns the current country code. */
     public synchronized String getCountryCode() {
-        return (mCurrentCountryCodeInfo != null) ? mCurrentCountryCodeInfo.getCountryCode() : null;
+        return mCurrentCountryCodeInfo.getCountryCode();
     }
 
     /**
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index 5664922..4c22278 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -18,6 +18,8 @@
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -58,15 +60,22 @@
         if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
             mPersistentSettings.initialize();
             mControllerService =
-                    ThreadNetworkControllerService.newInstance(mContext, mPersistentSettings);
+                    ThreadNetworkControllerService.newInstance(
+                            mContext, mPersistentSettings, () -> mCountryCode.getCountryCode());
+            mCountryCode =
+                    ThreadNetworkCountryCode.newInstance(
+                            mContext, mControllerService, mPersistentSettings);
             mControllerService.initialize();
         } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
             // Country code initialization is delayed to the BOOT_COMPLETED phase because it will
             // call into Wi-Fi and Telephony service whose country code module is ready after
             // PHASE_ACTIVITY_MANAGER_READY and PHASE_THIRD_PARTY_APPS_CAN_START
-            mCountryCode = ThreadNetworkCountryCode.newInstance(mContext, mControllerService);
             mCountryCode.initialize();
-            mShellCommand = new ThreadNetworkShellCommand(mCountryCode);
+            mShellCommand =
+                    new ThreadNetworkShellCommand(
+                            mContext,
+                            requireNonNull(mControllerService),
+                            requireNonNull(mCountryCode));
         }
     }
 
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
index c17c5a7..54155ee 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -17,36 +17,57 @@
 package com.android.server.thread;
 
 import android.annotation.Nullable;
-import android.os.Binder;
-import android.os.Process;
+import android.content.Context;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadNetworkException;
 import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.BasicShellCommandHandler;
+import com.android.net.module.util.HexDump;
 
 import java.io.PrintWriter;
-import java.util.List;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
- * Interprets and executes 'adb shell cmd thread_network [args]'.
+ * Interprets and executes 'adb shell cmd thread_network <subcommand>'.
+ *
+ * <p>Subcommands which don't have an equivalent Java API now require the
+ * "android.permission.THREAD_NETWORK_TESTING" permission. For a specific subcommand, it also
+ * requires the same permissions of the equivalent Java / AIDL API.
  *
  * <p>To add new commands: - onCommand: Add a case "<command>" execute. Return a 0 if command
  * executed successfully. - onHelp: add a description string.
- *
- * <p>Permissions: currently root permission is required for some commands. Others will enforce the
- * corresponding API permissions.
  */
-public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
-    private static final String TAG = "ThreadNetworkShellCommand";
+public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
+    private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
+    private static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
+    private static final Duration MIGRATE_TIMEOUT = Duration.ofSeconds(2);
+    private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1);
+    private static final String PERMISSION_THREAD_NETWORK_TESTING =
+            "android.permission.THREAD_NETWORK_TESTING";
 
-    // These don't require root access.
-    private static final List<String> NON_PRIVILEGED_COMMANDS = List.of("help", "get-country-code");
+    private final Context mContext;
+    private final ThreadNetworkControllerService mControllerService;
+    private final ThreadNetworkCountryCode mCountryCode;
 
-    @Nullable private final ThreadNetworkCountryCode mCountryCode;
     @Nullable private PrintWriter mOutputWriter;
     @Nullable private PrintWriter mErrorWriter;
 
-    ThreadNetworkShellCommand(@Nullable ThreadNetworkCountryCode countryCode) {
+    public ThreadNetworkShellCommand(
+            Context context,
+            ThreadNetworkControllerService controllerService,
+            ThreadNetworkCountryCode countryCode) {
+        mContext = context;
+        mControllerService = controllerService;
         mCountryCode = countryCode;
     }
 
@@ -65,77 +86,205 @@
     }
 
     @Override
+    public void onHelp() {
+        final PrintWriter pw = getOutputWriter();
+        pw.println("Thread network commands:");
+        pw.println("  help or -h");
+        pw.println("    Print this help text.");
+        pw.println("  enable");
+        pw.println("    Enables Thread radio");
+        pw.println("  disable");
+        pw.println("    Disables Thread radio");
+        pw.println("  join <active-dataset-tlvs>");
+        pw.println("    Joins a network of the given dataset");
+        pw.println("  migrate <active-dataset-tlvs> <delay-seconds>");
+        pw.println("    Migrate to the given network by a specific delay");
+        pw.println("  leave");
+        pw.println("    Leave the current network and erase datasets");
+        pw.println("  force-stop-ot-daemon enabled | disabled ");
+        pw.println("    force stop ot-daemon service");
+        pw.println("  get-country-code");
+        pw.println("    Gets country code as a two-letter string");
+        pw.println("  force-country-code enabled <two-letter code> | disabled ");
+        pw.println("    Sets country code to <two-letter code> or left for normal value");
+    }
+
+    @Override
     public int onCommand(String cmd) {
-        // Treat no command as help command.
+        // Treat no command as the "help" command
         if (TextUtils.isEmpty(cmd)) {
             cmd = "help";
         }
 
-        final PrintWriter pw = getOutputWriter();
-        final PrintWriter perr = getErrorWriter();
-
-        // Explicit exclusion from root permission
-        if (!NON_PRIVILEGED_COMMANDS.contains(cmd)) {
-            final int uid = Binder.getCallingUid();
-
-            if (uid != Process.ROOT_UID) {
-                perr.println(
-                        "Uid "
-                                + uid
-                                + " does not have access to "
-                                + cmd
-                                + " thread command "
-                                + "(or such command doesn't exist)");
-                return -1;
-            }
-        }
-
         switch (cmd) {
+            case "enable":
+                return setThreadEnabled(true);
+            case "disable":
+                return setThreadEnabled(false);
+            case "join":
+                return join();
+            case "leave":
+                return leave();
+            case "migrate":
+                return migrate();
+            case "force-stop-ot-daemon":
+                return forceStopOtDaemon();
             case "force-country-code":
-                boolean enabled;
-
-                if (mCountryCode == null) {
-                    perr.println("Thread country code operations are not supported");
-                    return -1;
-                }
-
-                try {
-                    enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
-                } catch (IllegalArgumentException e) {
-                    perr.println("Invalid argument: " + e.getMessage());
-                    return -1;
-                }
-
-                if (enabled) {
-                    String countryCode = getNextArgRequired();
-                    if (!ThreadNetworkCountryCode.isValidCountryCode(countryCode)) {
-                        perr.println(
-                                "Invalid argument: Country code must be a 2-Character"
-                                        + " string. But got country code "
-                                        + countryCode
-                                        + " instead");
-                        return -1;
-                    }
-                    mCountryCode.setOverrideCountryCode(countryCode);
-                    pw.println("Set Thread country code: " + countryCode);
-
-                } else {
-                    mCountryCode.clearOverrideCountryCode();
-                }
-                return 0;
+                return forceCountryCode();
             case "get-country-code":
-                if (mCountryCode == null) {
-                    perr.println("Thread country code operations are not supported");
-                    return -1;
-                }
-
-                pw.println("Thread country code = " + mCountryCode.getCountryCode());
-                return 0;
+                return getCountryCode();
             default:
                 return handleDefaultCommands(cmd);
         }
     }
 
+    private void ensureTestingPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                PERMISSION_THREAD_NETWORK_TESTING,
+                "Permission " + PERMISSION_THREAD_NETWORK_TESTING + " is missing!");
+    }
+
+    private int setThreadEnabled(boolean enabled) {
+        CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+        mControllerService.setEnabled(enabled, newOperationReceiver(setEnabledFuture));
+        return waitForFuture(setEnabledFuture, SET_ENABLED_TIMEOUT, getErrorWriter());
+    }
+
+    private int join() {
+        byte[] datasetTlvs = HexDump.hexStringToByteArray(getNextArgRequired());
+        ActiveOperationalDataset dataset;
+        try {
+            dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
+        } catch (IllegalArgumentException e) {
+            getErrorWriter().println("Invalid dataset argument: " + e.getMessage());
+            return -1;
+        }
+        // Do not wait for join to complete because this can take 8 to 30 seconds
+        mControllerService.join(dataset, new IOperationReceiver.Default());
+        return 0;
+    }
+
+    private int leave() {
+        CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+        mControllerService.leave(newOperationReceiver(leaveFuture));
+        return waitForFuture(leaveFuture, LEAVE_TIMEOUT, getErrorWriter());
+    }
+
+    private int migrate() {
+        byte[] datasetTlvs = HexDump.hexStringToByteArray(getNextArgRequired());
+        ActiveOperationalDataset dataset;
+        try {
+            dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
+        } catch (IllegalArgumentException e) {
+            getErrorWriter().println("Invalid dataset argument: " + e.getMessage());
+            return -1;
+        }
+
+        int delaySeconds;
+        try {
+            delaySeconds = Integer.parseInt(getNextArgRequired());
+        } catch (NumberFormatException e) {
+            getErrorWriter().println("Invalid delay argument: " + e.getMessage());
+            return -1;
+        }
+
+        PendingOperationalDataset pendingDataset =
+                new PendingOperationalDataset(
+                        dataset,
+                        OperationalDatasetTimestamp.fromInstant(Instant.now()),
+                        Duration.ofSeconds(delaySeconds));
+        CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+        mControllerService.scheduleMigration(pendingDataset, newOperationReceiver(migrateFuture));
+        return waitForFuture(migrateFuture, MIGRATE_TIMEOUT, getErrorWriter());
+    }
+
+    private int forceStopOtDaemon() {
+        ensureTestingPermission();
+        final PrintWriter errorWriter = getErrorWriter();
+        boolean enabled;
+        try {
+            enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
+        } catch (IllegalArgumentException e) {
+            errorWriter.println("Invalid argument: " + e.getMessage());
+            return -1;
+        }
+
+        CompletableFuture<Void> forceStopFuture = new CompletableFuture<>();
+        mControllerService.forceStopOtDaemonForTest(enabled, newOperationReceiver(forceStopFuture));
+        return waitForFuture(forceStopFuture, FORCE_STOP_TIMEOUT, getErrorWriter());
+    }
+
+    private int forceCountryCode() {
+        ensureTestingPermission();
+        final PrintWriter perr = getErrorWriter();
+        boolean enabled;
+        try {
+            enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
+        } catch (IllegalArgumentException e) {
+            perr.println("Invalid argument: " + e.getMessage());
+            return -1;
+        }
+
+        if (enabled) {
+            String countryCode = getNextArgRequired();
+            if (!ThreadNetworkCountryCode.isValidCountryCode(countryCode)) {
+                perr.println(
+                        "Invalid argument: Country code must be a 2-letter"
+                                + " string. But got country code "
+                                + countryCode
+                                + " instead");
+                return -1;
+            }
+            mCountryCode.setOverrideCountryCode(countryCode);
+        } else {
+            mCountryCode.clearOverrideCountryCode();
+        }
+        return 0;
+    }
+
+    private int getCountryCode() {
+        ensureTestingPermission();
+        getOutputWriter().println("Thread country code = " + mCountryCode.getCountryCode());
+        return 0;
+    }
+
+    private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
+        return new IOperationReceiver.Stub() {
+            @Override
+            public void onSuccess() {
+                future.complete(null);
+            }
+
+            @Override
+            public void onError(int errorCode, String errorMessage) {
+                future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+            }
+        };
+    }
+
+    /**
+     * Waits for the future to complete within given timeout.
+     *
+     * <p>Returns 0 if {@code future} completed successfully, or -1 if {@code future} failed to
+     * complete. When failed, error messages are printed to {@code errorWriter}.
+     */
+    private int waitForFuture(
+            CompletableFuture<Void> future, Duration timeout, PrintWriter errorWriter) {
+        try {
+            future.get(timeout.toSeconds(), TimeUnit.SECONDS);
+            return 0;
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            errorWriter.println("Failed: " + e.getMessage());
+        } catch (ExecutionException e) {
+            errorWriter.println("Failed: " + e.getCause().getMessage());
+        } catch (TimeoutException e) {
+            errorWriter.println("Failed: command timeout for " + timeout);
+        }
+
+        return -1;
+    }
+
     private static boolean argTrueOrFalse(String arg, String trueString, String falseString) {
         if (trueString.equals(arg)) {
             return true;
@@ -157,27 +306,4 @@
         String nextArg = getNextArgRequired();
         return argTrueOrFalse(nextArg, trueString, falseString);
     }
-
-    private void onHelpNonPrivileged(PrintWriter pw) {
-        pw.println("  get-country-code");
-        pw.println("    Gets country code as a two-letter string");
-    }
-
-    private void onHelpPrivileged(PrintWriter pw) {
-        pw.println("  force-country-code enabled <two-letter code> | disabled ");
-        pw.println("    Sets country code to <two-letter code> or left for normal value");
-    }
-
-    @Override
-    public void onHelp() {
-        final PrintWriter pw = getOutputWriter();
-        pw.println("Thread network commands:");
-        pw.println("  help or -h");
-        pw.println("    Print this help text.");
-        onHelpNonPrivileged(pw);
-        if (Binder.getCallingUid() == Process.ROOT_UID) {
-            onHelpPrivileged(pw);
-        }
-        pw.println();
-    }
 }
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index aba4193..7c4c72d 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -18,9 +18,11 @@
 
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ApexEnvironment;
 import android.content.Context;
+import android.net.thread.ThreadConfiguration;
 import android.os.PersistableBundle;
 import android.util.AtomicFile;
 import android.util.Log;
@@ -46,10 +48,13 @@
  */
 public class ThreadPersistentSettings {
     private static final String TAG = "ThreadPersistentSettings";
+
     /** File name used for storing settings. */
     private static final String FILE_NAME = "ThreadPersistentSettings.xml";
+
     /** Current config store data version. This will be incremented for any additions. */
     private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
+
     /**
      * Stores the version of the data. This can be used to handle migration of data if some
      * non-backward compatible change introduced.
@@ -58,7 +63,28 @@
 
     /******** Thread persistent setting keys ***************/
     /** Stores the Thread feature toggle state, true for enabled and false for disabled. */
-    public static final Key<Boolean> THREAD_ENABLED = new Key<>("Thread_enabled", true);
+    public static final Key<Boolean> THREAD_ENABLED = new Key<>("thread_enabled", true);
+
+    /**
+     * Indicates that Thread was enabled (i.e. via the setEnabled() API) when the airplane mode is
+     * turned on in settings. When this value is {@code true}, the current airplane mode state will
+     * be ignored when evaluating the Thread enabled state.
+     */
+    public static final Key<Boolean> THREAD_ENABLED_IN_AIRPLANE_MODE =
+            new Key<>("thread_enabled_in_airplane_mode", false);
+
+    /** Stores the Thread country code, null if no country code is stored. */
+    public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);
+
+    /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
+    private static final Key<Boolean> CONFIG_NAT64_ENABLED =
+            new Key<>("config_nat64_enabled", false);
+
+    /**
+     * Stores the Thread DHCPv6-PD feature toggle state, true for enabled and false for disabled.
+     */
+    private static final Key<Boolean> CONFIG_DHCP6_PD_ENABLED =
+            new Key<>("config_dhcp6_pd_enabled", false);
 
     /******** Thread persistent setting keys ***************/
 
@@ -120,7 +146,9 @@
     private <T> T getObject(String key, T defaultValue) {
         Object value;
         synchronized (mLock) {
-            if (defaultValue instanceof Boolean) {
+            if (defaultValue == null) {
+                value = mSettings.getString(key, null);
+            } else if (defaultValue instanceof Boolean) {
                 value = mSettings.getBoolean(key, (Boolean) defaultValue);
             } else if (defaultValue instanceof Integer) {
                 value = mSettings.getInt(key, (Integer) defaultValue);
@@ -159,6 +187,30 @@
     }
 
     /**
+     * Store a {@link ThreadConfiguration} to the persistent settings.
+     *
+     * @param configuration {@link ThreadConfiguration} to be stored.
+     * @return {@code true} if the configuration was changed, {@code false} otherwise.
+     */
+    public boolean putConfiguration(@NonNull ThreadConfiguration configuration) {
+        if (getConfiguration().equals(configuration)) {
+            return false;
+        }
+        putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
+        putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled());
+        writeToStoreFile();
+        return true;
+    }
+
+    /** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
+    public ThreadConfiguration getConfiguration() {
+        return new ThreadConfiguration.Builder()
+                .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
+                .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+                .build();
+    }
+
+    /**
      * Base class to store string key and its default value.
      *
      * @param <T> Type of the value.
@@ -210,7 +262,7 @@
                 mSettings.putAll(bundleRead);
             }
         } catch (FileNotFoundException e) {
-            Log.e(TAG, "No store file to read", e);
+            Log.w(TAG, "No store file to read", e);
         } catch (IOException e) {
             Log.e(TAG, "Read from store file failed", e);
         }
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index b29a54f..976f93d 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -16,29 +16,56 @@
 
 package com.android.server.thread;
 
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.EADDRINUSE;
+import static android.system.OsConstants.IFF_MULTICAST;
+import static android.system.OsConstants.IFF_NOARP;
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
+import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IFLA_AF_SPEC;
+import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IFLA_INET6_ADDR_GEN_MODE;
+import static com.android.net.module.util.netlink.RtNetlinkLinkMessage.IN6_ADDR_GEN_MODE_NONE;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
 import android.annotation.Nullable;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.RouteInfo;
-import android.net.util.SocketUtils;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
-import android.system.OsConstants;
 import android.util.Log;
 
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
 import com.android.net.module.util.netlink.NetlinkUtils;
-import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
+import com.android.net.module.util.netlink.StructIfinfoMsg;
+import com.android.net.module.util.netlink.StructNlAttr;
+import com.android.net.module.util.netlink.StructNlMsgHdr;
+import com.android.server.thread.openthread.Ipv6AddressInfo;
+import com.android.server.thread.openthread.OnMeshPrefixConfig;
 
-import java.io.FileDescriptor;
 import java.io.IOException;
-import java.io.InterruptedIOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
 
 /** Controller for virtual/tunnel network interfaces. */
 public class TunInterfaceController {
     private static final String TAG = "TunIfController";
+    private static final boolean DBG = false;
     private static final long INFINITE_LIFETIME = 0xffffffffL;
     static final int MTU = 1280;
 
@@ -48,15 +75,19 @@
 
     private final String mIfName;
     private final LinkProperties mLinkProperties = new LinkProperties();
+    private final MulticastSocket mMulticastSocket; // For join group and leave group
+    private final List<InetAddress> mMulticastAddresses = new ArrayList<>();
+    private final List<RouteInfo> mNetDataPrefixes = new ArrayList<>();
+
     private ParcelFileDescriptor mParcelTunFd;
-    private FileDescriptor mNetlinkSocket;
-    private static int sNetlinkSeqNo = 0;
+    private NetworkInterface mNetworkInterface;
 
     /** Creates a new {@link TunInterfaceController} instance for given interface. */
     public TunInterfaceController(String interfaceName) {
         mIfName = interfaceName;
         mLinkProperties.setInterfaceName(mIfName);
         mLinkProperties.setMtu(MTU);
+        mMulticastSocket = createMulticastSocket();
     }
 
     /** Returns link properties of the Thread TUN interface. */
@@ -72,21 +103,22 @@
     public void createTunInterface() throws IOException {
         mParcelTunFd = ParcelFileDescriptor.adoptFd(nativeCreateTunInterface(mIfName, MTU));
         try {
-            mNetlinkSocket = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_ROUTE);
-        } catch (ErrnoException e) {
-            throw new IOException("Failed to create netlink socket", e);
+            mNetworkInterface = NetworkInterface.getByName(mIfName);
+        } catch (SocketException e) {
+            throw new IOException("Failed to get NetworkInterface", e);
         }
+
+        setAddrGenModeToNone();
     }
 
     public void destroyTunInterface() {
         try {
             mParcelTunFd.close();
-            SocketUtils.closeSocket(mNetlinkSocket);
         } catch (IOException e) {
             // Should never fail
         }
         mParcelTunFd = null;
-        mNetlinkSocket = null;
+        mNetworkInterface = null;
     }
 
     /** Returns the FD of the tunnel interface. */
@@ -103,6 +135,10 @@
             for (LinkAddress address : mLinkProperties.getAllLinkAddresses()) {
                 removeAddress(address);
             }
+            for (RouteInfo route : mLinkProperties.getAllRoutes()) {
+                mLinkProperties.removeRoute(route);
+            }
+            mNetDataPrefixes.clear();
         }
         nativeSetInterfaceUp(mIfName, isUp);
     }
@@ -113,14 +149,14 @@
     public void addAddress(LinkAddress address) {
         Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
 
-        long validLifetimeSeconds;
         long preferredLifetimeSeconds;
+        long validLifetimeSeconds;
 
         if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
                 || address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
-            validLifetimeSeconds = INFINITE_LIFETIME;
+            preferredLifetimeSeconds = INFINITE_LIFETIME;
         } else {
-            validLifetimeSeconds =
+            preferredLifetimeSeconds =
                     Math.max(
                             (address.getDeprecationTime() - SystemClock.elapsedRealtime()) / 1000L,
                             0L);
@@ -128,28 +164,23 @@
 
         if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
                 || address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
-            preferredLifetimeSeconds = INFINITE_LIFETIME;
+            validLifetimeSeconds = INFINITE_LIFETIME;
         } else {
-            preferredLifetimeSeconds =
+            validLifetimeSeconds =
                     Math.max(
                             (address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
                             0L);
         }
 
-        byte[] message =
-                RtNetlinkAddressMessage.newRtmNewAddressMessage(
-                        sNetlinkSeqNo++,
-                        address.getAddress(),
-                        (short) address.getPrefixLength(),
-                        address.getFlags(),
-                        (byte) address.getScope(),
-                        Os.if_nametoindex(mIfName),
-                        validLifetimeSeconds,
-                        preferredLifetimeSeconds);
-        try {
-            Os.write(mNetlinkSocket, message, 0, message.length);
-        } catch (ErrnoException | InterruptedIOException e) {
-            Log.e(TAG, "Failed to add address " + address, e);
+        if (!NetlinkUtils.sendRtmNewAddressRequest(
+                Os.if_nametoindex(mIfName),
+                address.getAddress(),
+                (short) address.getPrefixLength(),
+                address.getFlags(),
+                (byte) address.getScope(),
+                preferredLifetimeSeconds,
+                validLifetimeSeconds)) {
+            Log.w(TAG, "Failed to add address " + address.getAddress().getHostAddress());
             return;
         }
         mLinkProperties.addLinkAddress(address);
@@ -159,32 +190,96 @@
     /** Removes an address from the interface. */
     public void removeAddress(LinkAddress address) {
         Log.d(TAG, "Removing address " + address);
-        byte[] message =
-                RtNetlinkAddressMessage.newRtmDelAddressMessage(
-                        sNetlinkSeqNo++,
-                        address.getAddress(),
-                        (short) address.getPrefixLength(),
-                        Os.if_nametoindex(mIfName));
 
         // Intentionally update the mLinkProperties before send netlink message because the
         // address is already removed from ot-daemon and apps can't reach to the address even
         // when the netlink request below fails
         mLinkProperties.removeLinkAddress(address);
         mLinkProperties.removeRoute(getRouteForAddress(address));
-        try {
-            Os.write(mNetlinkSocket, message, 0, message.length);
-        } catch (ErrnoException | InterruptedIOException e) {
-            Log.e(TAG, "Failed to remove address " + address, e);
+        if (!NetlinkUtils.sendRtmDelAddressRequest(
+                Os.if_nametoindex(mIfName),
+                (Inet6Address) address.getAddress(),
+                (short) address.getPrefixLength())) {
+            Log.w(TAG, "Failed to remove address " + address.getAddress().getHostAddress());
         }
     }
 
+    public void updateAddresses(List<Ipv6AddressInfo> addressInfoList) {
+        final List<LinkAddress> newLinkAddresses = new ArrayList<>();
+        final List<InetAddress> newMulticastAddresses = new ArrayList<>();
+        boolean hasActiveOmrAddress = false;
+
+        for (Ipv6AddressInfo addressInfo : addressInfoList) {
+            if (addressInfo.isActiveOmr) {
+                hasActiveOmrAddress = true;
+                break;
+            }
+        }
+
+        for (Ipv6AddressInfo addressInfo : addressInfoList) {
+            InetAddress address = addressInfoToInetAddress(addressInfo);
+            if (address.isMulticastAddress()) {
+                newMulticastAddresses.add(address);
+            } else {
+                newLinkAddresses.add(newLinkAddress(addressInfo, hasActiveOmrAddress));
+            }
+        }
+
+        final CompareResult<LinkAddress> addressDiff =
+                new CompareResult<>(mLinkProperties.getAllLinkAddresses(), newLinkAddresses);
+        for (LinkAddress linkAddress : addressDiff.removed) {
+            removeAddress(linkAddress);
+        }
+        for (LinkAddress linkAddress : addressDiff.added) {
+            addAddress(linkAddress);
+        }
+
+        final CompareResult<InetAddress> multicastAddressDiff =
+                new CompareResult<>(mMulticastAddresses, newMulticastAddresses);
+        for (InetAddress address : multicastAddressDiff.removed) {
+            leaveGroup(address);
+        }
+        for (InetAddress address : multicastAddressDiff.added) {
+            joinGroup(address);
+        }
+        mMulticastAddresses.clear();
+        mMulticastAddresses.addAll(newMulticastAddresses);
+    }
+
+    public void updatePrefixes(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
+        final List<RouteInfo> newNetDataPrefixes = new ArrayList<>();
+
+        for (OnMeshPrefixConfig onMeshPrefixConfig : onMeshPrefixConfigList) {
+            newNetDataPrefixes.add(getRouteForOnMeshPrefix(onMeshPrefixConfig));
+        }
+
+        final CompareResult<RouteInfo> prefixDiff =
+                new CompareResult<>(mNetDataPrefixes, newNetDataPrefixes);
+        for (RouteInfo routeRemoved : prefixDiff.removed) {
+            mLinkProperties.removeRoute(routeRemoved);
+        }
+        for (RouteInfo routeAdded : prefixDiff.added) {
+            mLinkProperties.addRoute(routeAdded);
+        }
+
+        mNetDataPrefixes.clear();
+        mNetDataPrefixes.addAll(newNetDataPrefixes);
+    }
+
     private RouteInfo getRouteForAddress(LinkAddress linkAddress) {
-        return new RouteInfo(
-                new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()),
-                null,
-                mIfName,
-                RouteInfo.RTN_UNICAST,
-                MTU);
+        return getRouteForIpPrefix(
+                new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()));
+    }
+
+    private RouteInfo getRouteForOnMeshPrefix(OnMeshPrefixConfig onMeshPrefixConfig) {
+        return getRouteForIpPrefix(
+                new IpPrefix(
+                        bytesToInet6Address(onMeshPrefixConfig.prefix),
+                        onMeshPrefixConfig.prefixLength));
+    }
+
+    private RouteInfo getRouteForIpPrefix(IpPrefix ipPrefix) {
+        return new RouteInfo(ipPrefix, null, mIfName, RouteInfo.RTN_UNICAST, MTU);
     }
 
     /** Called by {@link ThreadNetworkControllerService} to do clean up when ot-daemon is dead. */
@@ -195,4 +290,139 @@
             Log.e(TAG, "Failed to set Thread TUN interface down");
         }
     }
+
+    private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
+        return bytesToInet6Address(addressInfo.address);
+    }
+
+    private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
+        try {
+            return (Inet6Address) Inet6Address.getByAddress(addressBytes);
+        } catch (UnknownHostException e) {
+            // This is unlikely to happen unless the Thread daemon is critically broken
+            return null;
+        }
+    }
+
+    private static LinkAddress newLinkAddress(
+            Ipv6AddressInfo addressInfo, boolean hasActiveOmrAddress) {
+        // Mesh-local addresses and OMR address have the same scope, to distinguish them we set
+        // mesh-local addresses as deprecated when there is an active OMR address.
+        // For OMR address and link-local address we only use the value isPreferred set by
+        // ot-daemon.
+        boolean isPreferred = addressInfo.isPreferred;
+        if (addressInfo.isMeshLocal && hasActiveOmrAddress) {
+            isPreferred = false;
+        }
+
+        final long deprecationTimeMillis =
+                isPreferred ? LinkAddress.LIFETIME_PERMANENT : SystemClock.elapsedRealtime();
+
+        final InetAddress address = addressInfoToInetAddress(addressInfo);
+
+        // flags and scope will be adjusted automatically depending on the address and
+        // its lifetimes.
+        return new LinkAddress(
+                address,
+                addressInfo.prefixLength,
+                0 /* flags */,
+                0 /* scope */,
+                deprecationTimeMillis,
+                LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
+    }
+
+    private MulticastSocket createMulticastSocket() {
+        try {
+            return new MulticastSocket();
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to create multicast socket ", e);
+        }
+    }
+
+    private void joinGroup(InetAddress address) {
+        InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+        try {
+            mMulticastSocket.joinGroup(socketAddress, mNetworkInterface);
+        } catch (IOException e) {
+            if (e.getCause() instanceof ErrnoException) {
+                ErrnoException ee = (ErrnoException) e.getCause();
+                if (ee.errno == EADDRINUSE) {
+                    Log.w(TAG, "Already joined group" + address.getHostAddress(), e);
+                    return;
+                }
+            }
+            Log.e(TAG, "failed to join group " + address.getHostAddress(), e);
+        }
+    }
+
+    private void leaveGroup(InetAddress address) {
+        InetSocketAddress socketAddress = new InetSocketAddress(address, 0);
+        try {
+            mMulticastSocket.leaveGroup(socketAddress, mNetworkInterface);
+        } catch (IOException e) {
+            Log.e(TAG, "failed to leave group " + address.getHostAddress(), e);
+        }
+    }
+
+    /**
+     * Sets the address generation mode to {@code IN6_ADDR_GEN_MODE_NONE}.
+     *
+     * <p>So that the "thread-wpan" interface has only one IPv6 link local address which is
+     * generated by OpenThread.
+     */
+    private void setAddrGenModeToNone() {
+        StructNlMsgHdr header = new StructNlMsgHdr();
+        header.nlmsg_type = RTM_NEWLINK;
+        header.nlmsg_pid = 0;
+        header.nlmsg_seq = 0;
+        header.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+
+        StructIfinfoMsg ifInfo =
+                new StructIfinfoMsg(
+                        (short) 0 /* family */,
+                        0 /* type */,
+                        Os.if_nametoindex(mIfName),
+                        (IFF_MULTICAST | IFF_NOARP) /* flags */,
+                        0xffffffff /* change */);
+
+        // Nested attributes
+        // IFLA_AF_SPEC
+        //   AF_INET6
+        //     IFLA_INET6_ADDR_GEN_MODE
+        StructNlAttr addrGenMode =
+                new StructNlAttr(IFLA_INET6_ADDR_GEN_MODE, (byte) IN6_ADDR_GEN_MODE_NONE);
+        StructNlAttr afInet6 = new StructNlAttr((short) AF_INET6, addrGenMode);
+        StructNlAttr afSpec = new StructNlAttr(IFLA_AF_SPEC, afInet6);
+
+        final int msgLength =
+                StructNlMsgHdr.STRUCT_SIZE
+                        + StructIfinfoMsg.STRUCT_SIZE
+                        + afSpec.getAlignedLength();
+        byte[] msg = new byte[msgLength];
+        ByteBuffer buf = ByteBuffer.wrap(msg);
+        buf.order(ByteOrder.nativeOrder());
+
+        header.nlmsg_len = msgLength;
+        header.pack(buf);
+        ifInfo.pack(buf);
+        afSpec.pack(buf);
+
+        if (buf.position() != msgLength) {
+            throw new AssertionError(
+                    String.format(
+                            "Unexpected netlink message size (actual = %d, expected = %d)",
+                            buf.position(), msgLength));
+        }
+
+        if (DBG) {
+            Log.d(TAG, "ADDR_GEN_MODE message is:");
+            Log.d(TAG, HexDump.dumpHexString(msg));
+        }
+
+        try {
+            NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Failed to set ADDR_GEN_MODE to NONE", e);
+        }
+    }
 }
diff --git a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
index 5d24eab..1f260f2 100644
--- a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
+++ b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
@@ -42,15 +42,8 @@
 
 namespace android {
 static jint
-com_android_server_thread_InfraInterfaceController_createIcmp6Socket(JNIEnv *env, jobject clazz,
-                                                                     jstring interfaceName) {
-  ScopedUtfChars ifName(env, interfaceName);
-
-  struct icmp6_filter filter;
-  constexpr int kEnable = 1;
-  constexpr int kIpv6ChecksumOffset = 2;
-  constexpr int kHopLimit = 255;
-
+com_android_server_thread_InfraInterfaceController_createFilteredIcmp6Socket(JNIEnv *env,
+                                                                             jobject clazz) {
   // Initializes the ICMPv6 socket.
   int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
   if (sock == -1) {
@@ -59,6 +52,7 @@
     return -1;
   }
 
+  struct icmp6_filter filter;
   // Only accept Router Advertisements, Router Solicitations and Neighbor
   // Advertisements.
   ICMP6_FILTER_SETBLOCKALL(&filter);
@@ -73,53 +67,6 @@
     return -1;
   }
 
-  // We want a source address and interface index.
-
-  if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &kEnable, sizeof(kEnable)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVPKTINFO (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  if (setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &kIpv6ChecksumOffset,
-                 sizeof(kIpv6ChecksumOffset)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_CHECKSUM (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  // We need to be able to reject RAs arriving from off-link.
-  if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &kEnable, sizeof(kEnable)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVHOPLIMIT (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_UNICAST_HOPS (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
-    jniThrowExceptionFmt(env, "java/io/IOException",
-                         "failed to create the setsockopt IPV6_MULTICAST_HOPS (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
-  if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifName.c_str(), strlen(ifName.c_str()))) {
-    jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt SO_BINDTODEVICE (%s)",
-                         strerror(errno));
-    close(sock);
-    return -1;
-  }
-
   return sock;
 }
 
@@ -129,8 +76,8 @@
 
 static const JNINativeMethod gMethods[] = {
     /* name, signature, funcPtr */
-    {"nativeCreateIcmp6Socket", "(Ljava/lang/String;)I",
-     (void *)com_android_server_thread_InfraInterfaceController_createIcmp6Socket},
+    {"nativeCreateFilteredIcmp6Socket", "()I",
+     (void *)com_android_server_thread_InfraInterfaceController_createFilteredIcmp6Socket},
 };
 
 int register_com_android_server_thread_InfraInterfaceController(JNIEnv *env) {
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index c1cf0a0..6db7c9c 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -21,9 +21,11 @@
 
 android_test {
     name: "CtsThreadNetworkTestCases",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
     min_sdk_version: "33",
-    sdk_version: "test_current",
     manifest: "AndroidManifest.xml",
     test_config: "AndroidTest.xml",
     srcs: [
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index ffc181c..6eda1e9 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -38,6 +38,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <!-- Install test -->
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="CtsThreadNetworkTestCases.apk" />
diff --git a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
index 0e76930..996d22d 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
@@ -30,6 +30,8 @@
 import android.net.thread.ActiveOperationalDataset.Builder;
 import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
 import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 import android.util.SparseArray;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -38,6 +40,7 @@
 import com.google.common.primitives.Bytes;
 import com.google.common.testing.EqualsTester;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -46,6 +49,7 @@
 
 /** CTS tests for {@link ActiveOperationalDataset}. */
 @SmallTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public final class ActiveOperationalDatasetTest {
     private static final int TYPE_ACTIVE_TIMESTAMP = 14;
@@ -81,6 +85,8 @@
     private static final ActiveOperationalDataset DEFAULT_DATASET =
             ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
 
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     private static byte[] removeTlv(byte[] dataset, int type) {
         ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length);
         int i = 0;
diff --git a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
index 9be3d56..4d7c7f1 100644
--- a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
@@ -21,12 +21,15 @@
 import static org.junit.Assert.assertThrows;
 
 import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.google.common.testing.EqualsTester;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -34,8 +37,11 @@
 
 /** Tests for {@link OperationalDatasetTimestamp}. */
 @SmallTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public final class OperationalDatasetTimestampTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     @Test
     public void fromInstant_tooLargeInstant_throwsIllegalArgument() {
         assertThrows(
diff --git a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
index 0bb18ce..76be054 100644
--- a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
@@ -28,6 +28,8 @@
 import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 import android.util.SparseArray;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -36,6 +38,7 @@
 import com.google.common.primitives.Bytes;
 import com.google.common.testing.EqualsTester;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -44,8 +47,11 @@
 
 /** Tests for {@link PendingOperationalDataset}. */
 @SmallTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public final class PendingOperationalDatasetTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     private static ActiveOperationalDataset createActiveDataset() throws Exception {
         SparseArray<byte[]> channelMask = new SparseArray<>(1);
         channelMask.put(0, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
new file mode 100644
index 0000000..386412e
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.thread.ThreadConfiguration;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Tests for {@link ThreadConfiguration}. */
+@SmallTest
+@RequiresThreadFeature
+@RunWith(Parameterized.class)
+public final class ThreadConfigurationTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    public final boolean mIsNat64Enabled;
+    public final boolean mIsDhcpv6PdEnabled;
+
+    @Parameterized.Parameters
+    public static Collection configArguments() {
+        return Arrays.asList(
+                new Object[][] {
+                    {false, false}, // All disabled
+                    {true, false}, // NAT64 enabled
+                    {false, true}, // DHCP6-PD enabled
+                    {true, true}, // All enabled
+                });
+    }
+
+    public ThreadConfigurationTest(boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
+        mIsNat64Enabled = isNat64Enabled;
+        mIsDhcpv6PdEnabled = isDhcpv6PdEnabled;
+    }
+
+    @Test
+    public void parcelable_parcelingIsLossLess() {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+        assertParcelingIsLossless(config);
+    }
+
+    @Test
+    public void builder_correctValuesAreSet() {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+
+        assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled);
+        assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled);
+    }
+
+    @Test
+    public void builderConstructor_configsAreEqual() {
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(mIsNat64Enabled)
+                        .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
+                        .build();
+        ThreadConfiguration config2 = new ThreadConfiguration.Builder(config1).build();
+        assertThat(config1).isEqualTo(config2);
+    }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 36ce4d5..1a101b6 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -17,6 +17,12 @@
 package android.net.thread.cts;
 
 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
@@ -37,10 +43,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeNotNull;
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
@@ -54,12 +58,15 @@
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
 import android.net.thread.ThreadNetworkController;
 import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
 import android.net.thread.ThreadNetworkController.StateCallback;
 import android.net.thread.ThreadNetworkException;
 import android.net.thread.ThreadNetworkManager;
 import android.net.thread.utils.TapTestNetworkTracker;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 import android.os.Build;
 import android.os.HandlerThread;
 import android.os.OutcomeReceiver;
@@ -69,16 +76,13 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.net.module.util.ArrayTrackRecord;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.FunctionalUtils.ThrowingRunnable;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
@@ -92,15 +96,16 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /** CTS tests for {@link ThreadNetworkController}. */
 @LargeTest
-@RunWith(DevSdkIgnoreRunner.class)
-@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
+@RequiresThreadFeature
 public class ThreadNetworkControllerTest {
     private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
     private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
@@ -108,12 +113,16 @@
     private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
     private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
     private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
+    private static final int SET_CONFIGURATION_TIMEOUT_MILLIS = 1_000;
     private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
+    private static final int SERVICE_LOST_TIMEOUT_MILLIS = 20_000;
     private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
     private static final String THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
+    private static final ThreadConfiguration DEFAULT_CONFIG =
+            new ThreadConfiguration.Builder().build();
 
-    @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
 
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private ExecutorService mExecutor;
@@ -124,16 +133,15 @@
     private HandlerThread mHandlerThread;
     private TapTestNetworkTracker mTestNetworkTracker;
 
+    private final List<Consumer<ThreadConfiguration>> mConfigurationCallbacksToCleanUp =
+            new ArrayList<>();
+
     @Before
     public void setUp() throws Exception {
-        ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
-        if (manager != null) {
-            mController = manager.getAllThreadNetworkControllers().get(0);
-        }
-
-        // TODO: we will also need it in tearDown(), it's better to have a Rule to skip
-        // tests if a feature is not available.
-        assumeNotNull(mController);
+        mController =
+                mContext.getSystemService(ThreadNetworkManager.class)
+                        .getAllThreadNetworkControllers()
+                        .get(0);
 
         mGrantedPermissions = new HashSet<String>();
         mExecutor = Executors.newSingleThreadExecutor();
@@ -142,16 +150,26 @@
         mHandlerThread.start();
 
         setEnabledAndWait(mController, true);
+        setConfigurationAndWait(mController, DEFAULT_CONFIG);
     }
 
     @After
     public void tearDown() throws Exception {
-        if (mController == null) {
-            return;
-        }
         dropAllPermissions();
         leaveAndWait(mController);
         tearDownTestNetwork();
+        setConfigurationAndWait(mController, DEFAULT_CONFIG);
+        for (Consumer<ThreadConfiguration> configurationCallback :
+                mConfigurationCallbacksToCleanUp) {
+            try {
+                runAsShell(
+                        THREAD_NETWORK_PRIVILEGED,
+                        () -> mController.unregisterConfigurationCallback(configurationCallback));
+            } catch (IllegalArgumentException e) {
+                // Ignore the exception when the callback is not registered.
+            }
+        }
+        mConfigurationCallbacksToCleanUp.clear();
     }
 
     @Test
@@ -177,6 +195,17 @@
     }
 
     @Test
+    public void subscribeThreadEnableState_getActiveDataset_onThreadEnableStateChangedNotCalled()
+            throws Exception {
+        EnabledStateListener listener = new EnabledStateListener(mController);
+        listener.expectThreadEnabledState(STATE_ENABLED);
+
+        getActiveOperationalDataset(mController);
+
+        listener.expectCallbackNotCalled();
+    }
+
+    @Test
     public void registerStateCallback_returnsUpdatedEnabledStates() throws Exception {
         CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
         CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
@@ -754,17 +783,6 @@
     }
 
     @Test
-    public void setEnabled_toggleAfterJoin_joinsThreadNetworkAgain() throws Exception {
-        joinRandomizedDatasetAndWait(mController);
-
-        setEnabledAndWait(mController, false);
-        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
-        setEnabledAndWait(mController, true);
-
-        runAsShell(ACCESS_NETWORK_STATE, () -> waitForAttachedState(mController));
-    }
-
-    @Test
     public void setEnabled_enableFollowedByDisable_allSucceed() throws Exception {
         joinRandomizedDatasetAndWait(mController);
         CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
@@ -797,10 +815,14 @@
     public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception {
         CompletableFuture<Network> networkFuture = new CompletableFuture<>();
         ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
-        NetworkRequest networkRequest =
-                new NetworkRequest.Builder()
-                        .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
-                        .build();
+        NetworkRequest.Builder networkRequestBuilder =
+                new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_THREAD);
+        // Before V, we need to explicitly set `NET_CAPABILITY_LOCAL_NETWORK` capability to request
+        // a Thread network.
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            networkRequestBuilder.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        }
+        NetworkRequest networkRequest = networkRequestBuilder.build();
         ConnectivityManager.NetworkCallback networkCallback =
                 new ConnectivityManager.NetworkCallback() {
                     @Override
@@ -816,6 +838,166 @@
 
         assertThat(isAttached(mController)).isTrue();
         assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull();
+        NetworkCapabilities caps =
+                runAsShell(
+                        ACCESS_NETWORK_STATE, () -> cm.getNetworkCapabilities(networkFuture.get()));
+        assertThat(caps).isNotNull();
+        assertThat(caps.hasTransport(NetworkCapabilities.TRANSPORT_THREAD)).isTrue();
+        assertThat(caps.getCapabilities())
+                .asList()
+                .containsAtLeast(
+                        NET_CAPABILITY_LOCAL_NETWORK,
+                        NET_CAPABILITY_NOT_METERED,
+                        NET_CAPABILITY_NOT_RESTRICTED,
+                        NET_CAPABILITY_NOT_VCN_MANAGED,
+                        NET_CAPABILITY_NOT_VPN,
+                        NET_CAPABILITY_TRUSTED);
+    }
+
+    @Test
+    public void setConfiguration_null_throwsNullPointerException() throws Exception {
+        CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        mController.setConfiguration(
+                                null, mExecutor, newOutcomeReceiver(setConfigFuture)));
+    }
+
+    @Test
+    public void setConfiguration_noPermissions_throwsSecurityException() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+        CompletableFuture<Void> setConfigFuture = new CompletableFuture<>();
+        assertThrows(
+                SecurityException.class,
+                () -> {
+                    mController.setConfiguration(
+                            configuration, mExecutor, newOutcomeReceiver(setConfigFuture));
+                });
+    }
+
+    @Test
+    public void registerConfigurationCallback_permissionsGranted_returnsCurrentStatus()
+            throws Exception {
+        CompletableFuture<ThreadConfiguration> getConfigFuture = new CompletableFuture<>();
+        Consumer<ThreadConfiguration> callback = getConfigFuture::complete;
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+        assertThat(getConfigFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+                .isEqualTo(DEFAULT_CONFIG);
+    }
+
+    @Test
+    public void registerConfigurationCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () -> registerConfigurationCallback(mController, mExecutor, config -> {}));
+    }
+
+    @Test
+    public void registerConfigurationCallback_returnsUpdatedConfigurations() throws Exception {
+        CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+        CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+        ConfigurationListener listener = new ConfigurationListener(mController);
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+        ThreadConfiguration config2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+
+        try {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () ->
+                            mController.setConfiguration(
+                                    config1, mExecutor, newOutcomeReceiver(setFuture1)));
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () ->
+                            mController.setConfiguration(
+                                    config2, mExecutor, newOutcomeReceiver(setFuture2)));
+            setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+            setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+            listener.expectConfiguration(DEFAULT_CONFIG);
+            listener.expectConfiguration(config1);
+            listener.expectConfiguration(config2);
+            listener.expectNoMoreConfiguration();
+        } finally {
+            listener.unregisterConfigurationCallback();
+        }
+    }
+
+    @Test
+    public void registerConfigurationCallback_alreadyRegistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        Consumer<ThreadConfiguration> callback = config -> {};
+        registerConfigurationCallback(mController, mExecutor, callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_noPermissions_throwsSecurityException()
+            throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> registerConfigurationCallback(mController, mExecutor, callback));
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_callbackRegistered_success() throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    registerConfigurationCallback(mController, mExecutor, callback);
+                    mController.unregisterConfigurationCallback(callback);
+                });
+    }
+
+    @Test
+    public void
+            unregisterConfigurationCallback_callbackNotRegistered_throwsIllegalArgumentException()
+                    throws Exception {
+        Consumer<ThreadConfiguration> callback = config -> {};
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
+    }
+
+    @Test
+    public void unregisterConfigurationCallback_alreadyUnregistered_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        Consumer<ThreadConfiguration> callback = config -> {};
+        registerConfigurationCallback(mController, mExecutor, callback);
+        mController.unregisterConfigurationCallback(callback);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.unregisterConfigurationCallback(callback));
     }
 
     private void grantPermissions(String... permissions) {
@@ -848,6 +1030,7 @@
     }
 
     @Test
+    @Ignore("b/333649897: Enable this when it's not flaky at all")
     public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception {
         setUpTestNetwork();
 
@@ -875,21 +1058,23 @@
     @Test
     public void meshcopService_threadDisabled_notDiscovered() throws Exception {
         setUpTestNetwork();
-
         CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
         NsdManager.DiscoveryListener listener =
                 discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture);
+
         setEnabledAndWait(mController, false);
+
         try {
-            serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+            serviceLostFuture.get(SERVICE_LOST_TIMEOUT_MILLIS, MILLISECONDS);
         } catch (InterruptedException | ExecutionException | TimeoutException ignored) {
             // It's fine if the service lost event didn't show up. The service may not ever be
             // advertised.
         } finally {
             mNsdManager.stopServiceDiscovery(listener);
         }
-
-        assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
+        assertThrows(
+                TimeoutException.class,
+                () -> discoverService(MESHCOP_SERVICE_TYPE, SERVICE_LOST_TIMEOUT_MILLIS));
     }
 
     private static void dropAllPermissions() {
@@ -1009,7 +1194,11 @@
         }
 
         public void expectThreadEnabledState(int enabled) {
-            assertNotNull(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled)));
+            assertThat(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled))).isNotNull();
+        }
+
+        public void expectCallbackNotCalled() {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, e -> true)).isNull();
         }
 
         public void unregisterStateCallback() {
@@ -1017,6 +1206,35 @@
         }
     }
 
+    private class ConfigurationListener {
+        private ArrayTrackRecord<ThreadConfiguration> mConfigurations = new ArrayTrackRecord<>();
+        private final ArrayTrackRecord<ThreadConfiguration>.ReadHead mReadHead =
+                mConfigurations.newReadHead();
+        ThreadNetworkController mController;
+        Consumer<ThreadConfiguration> mCallback = (config) -> mConfigurations.add(config);
+
+        ConfigurationListener(ThreadNetworkController controller) {
+            this.mController = controller;
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> controller.registerConfigurationCallback(mExecutor, mCallback));
+        }
+
+        public void expectConfiguration(ThreadConfiguration config) {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> c.equals(config))).isNotNull();
+        }
+
+        public void expectNoMoreConfiguration() {
+            assertThat(mReadHead.poll(CALLBACK_TIMEOUT_MILLIS, c -> true)).isNull();
+        }
+
+        public void unregisterConfigurationCallback() {
+            runAsShell(
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> mController.unregisterConfigurationCallback(mCallback));
+        }
+    }
+
     private int booleanToEnabledState(boolean enabled) {
         return enabled ? STATE_ENABLED : STATE_DISABLED;
     }
@@ -1031,6 +1249,18 @@
         waitForEnabledState(controller, booleanToEnabledState(enabled));
     }
 
+    private void setConfigurationAndWait(
+            ThreadNetworkController controller, ThreadConfiguration configuration)
+            throws Exception {
+        CompletableFuture<Void> setFuture = new CompletableFuture<>();
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        controller.setConfiguration(
+                                configuration, mExecutor, newOutcomeReceiver(setFuture)));
+        setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
     private CompletableFuture joinRandomizedDataset(
             ThreadNetworkController controller, String networkName) throws Exception {
         ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
@@ -1097,6 +1327,14 @@
         };
     }
 
+    private void registerConfigurationCallback(
+            ThreadNetworkController controller,
+            Executor executor,
+            Consumer<ThreadConfiguration> callback) {
+        controller.registerConfigurationCallback(executor, callback);
+        mConfigurationCallbacksToCleanUp.add(callback);
+    }
+
     private static void assertDoesNotThrow(ThrowingRunnable runnable) {
         try {
             runnable.run();
@@ -1107,6 +1345,12 @@
 
     // Return the first discovered service instance.
     private NsdServiceInfo discoverService(String serviceType) throws Exception {
+        return discoverService(serviceType, SERVICE_DISCOVERY_TIMEOUT_MILLIS);
+    }
+
+    // Return the first discovered service instance.
+    private NsdServiceInfo discoverService(String serviceType, int timeoutMilliseconds)
+            throws Exception {
         CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
         NsdManager.DiscoveryListener listener =
                 new DefaultDiscoveryListener() {
@@ -1115,9 +1359,14 @@
                         serviceInfoFuture.complete(serviceInfo);
                     }
                 };
-        mNsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+        mNsdManager.discoverServices(
+                serviceType,
+                NsdManager.PROTOCOL_DNS_SD,
+                mTestNetworkTracker.getNetwork(),
+                mExecutor,
+                listener);
         try {
-            serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+            serviceInfoFuture.get(timeoutMilliseconds, MILLISECONDS);
         } finally {
             mNsdManager.stopServiceDiscovery(listener);
         }
@@ -1134,7 +1383,12 @@
                         serviceInfoFuture.complete(serviceInfo);
                     }
                 };
-        mNsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+        mNsdManager.discoverServices(
+                serviceType,
+                NsdManager.PROTOCOL_DNS_SD,
+                mTestNetworkTracker.getNetwork(),
+                mExecutor,
+                listener);
         return listener;
     }
 
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
index 7d9ae81..4de2e13 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
@@ -25,17 +25,23 @@
 import static org.junit.Assert.assertThrows;
 
 import android.net.thread.ThreadNetworkException;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 /** CTS tests for {@link ThreadNetworkException}. */
 @SmallTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public final class ThreadNetworkExceptionTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     @Test
     public void constructor_validValues_valuesAreConnectlySet() throws Exception {
         ThreadNetworkException errorThreadDisabled =
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 6ba192d..71693af 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -30,8 +30,11 @@
         "net-tests-utils",
         "net-utils-device-common",
         "net-utils-device-common-bpf",
+        "net-utils-device-common-struct-base",
         "testables",
+        "ThreadNetworkTestUtils",
         "truth",
+        "ot-daemon-aidl-java",
     ],
     libs: [
         "android.test.runner",
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
index 152c1c3..8f98941 100644
--- a/thread/tests/integration/AndroidTest.xml
+++ b/thread/tests/integration/AndroidTest.xml
@@ -31,6 +31,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
 
     <!-- Install test -->
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index e8ef346..9e8dc3a 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -17,46 +17,52 @@
 package android.net.thread;
 
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
-import static android.Manifest.permission.NETWORK_SETTINGS;
-import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
-import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
+import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
 import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
-import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isFrom;
 import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
-import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
-import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
+import static android.net.thread.utils.IntegrationTestUtils.isTo;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
 import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
 import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
 import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+import static android.system.OsConstants.ICMP_ECHO;
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assume.assumeNotNull;
-import static org.junit.Assume.assumeTrue;
 
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.Objects.requireNonNull;
 
 import android.content.Context;
-import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.MacAddress;
+import android.net.RouteInfo;
 import android.net.thread.utils.FullThreadDevice;
 import android.net.thread.utils.InfraNetworkDevice;
+import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresIpv6MulticastRouting;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.SystemClock;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.LargeTest;
@@ -67,25 +73,42 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 
 /** Integration test cases for Thread Border Routing feature. */
 @RunWith(AndroidJUnit4.class)
+@RequiresThreadFeature
+@RequiresSimulationThreadDevice
 @LargeTest
 public class BorderRoutingTest {
     private static final String TAG = BorderRoutingTest.class.getSimpleName();
+    private static final int NUM_FTD = 2;
+    private static final Inet6Address GROUP_ADDR_SCOPE_5 =
+            (Inet6Address) parseNumericAddress("ff05::1234");
+    private static final Inet6Address GROUP_ADDR_SCOPE_4 =
+            (Inet6Address) parseNumericAddress("ff04::1234");
+    private static final Inet6Address GROUP_ADDR_SCOPE_3 =
+            (Inet6Address) parseNumericAddress("ff03::1234");
+    private static final Inet4Address IPV4_SERVER_ADDR =
+            (Inet4Address) parseNumericAddress("8.8.8.8");
+    private static final String NAT64_CIDR = "192.168.255.0/24";
+
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
     private final Context mContext = ApplicationProvider.getApplicationContext();
-    private ThreadNetworkController mController;
+    private final ThreadNetworkControllerWrapper mController =
+            ThreadNetworkControllerWrapper.newInstance(mContext);
+    private OtDaemonController mOtCtl;
     private HandlerThread mHandlerThread;
     private Handler mHandler;
     private TestNetworkTracker mInfraNetworkTracker;
@@ -93,36 +116,11 @@
     private TapPacketReader mInfraNetworkReader;
     private InfraNetworkDevice mInfraDevice;
 
-    private static final int NUM_FTD = 2;
-    private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
-    private static final Inet6Address GROUP_ADDR_SCOPE_5 =
-            (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
-    private static final Inet6Address GROUP_ADDR_SCOPE_4 =
-            (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
-    private static final Inet6Address GROUP_ADDR_SCOPE_3 =
-            (Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
-
-    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
-    private static final byte[] DEFAULT_DATASET_TLVS =
-            base16().decode(
-                            "0E080000000000010000000300001335060004001FFFE002"
-                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
-                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
-                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
-                                    + "B9D351B40C0402A0FFF8");
-    private static final ActiveOperationalDataset DEFAULT_DATASET =
-            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
-
     @Before
     public void setUp() throws Exception {
-        assumeTrue(isSimulatedThreadRadioSupported());
-        final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
-        if (manager != null) {
-            mController = manager.getAllThreadNetworkControllers().get(0);
-        }
-
-        // Run the tests on only devices where the Thread feature is available
-        assumeNotNull(mController);
+        // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+        mOtCtl = new OtDaemonController();
+        mOtCtl.factoryReset();
 
         mHandlerThread = new HandlerThread(getClass().getSimpleName());
         mHandlerThread.start();
@@ -130,13 +128,12 @@
         mFtds = new ArrayList<>();
 
         setUpInfraNetwork();
-
-        // BR forms a network.
-        startBrLeader();
+        mController.setEnabledAndWait(true);
+        mController.joinAndWait(DEFAULT_DATASET);
 
         // Creates a infra network device.
         mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
-        startInfraDevice();
+        startInfraDeviceAndWaitForOnLinkAddr();
 
         // Create Ftds
         for (int i = 0; i < NUM_FTD; ++i) {
@@ -146,20 +143,8 @@
 
     @After
     public void tearDown() throws Exception {
-        if (mController == null) {
-            return;
-        }
-
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                NETWORK_SETTINGS,
-                () -> {
-                    CountDownLatch latch = new CountDownLatch(2);
-                    mController.setTestNetworkAsUpstream(
-                            null, directExecutor(), v -> latch.countDown());
-                    mController.leave(directExecutor(), v -> latch.countDown());
-                    latch.await(10, TimeUnit.SECONDS);
-                });
+        mController.setTestNetworkAsUpstreamAndWait(null);
+        mController.leaveAndWait();
         tearDownInfraNetwork();
 
         mHandlerThread.quitSafely();
@@ -172,7 +157,7 @@
     }
 
     @Test
-    public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
+    public void unicastRouting_infraDevicePingThreadDeviceOmr_replyReceived() throws Exception {
         /*
          * <pre>
          * Topology:
@@ -182,22 +167,69 @@
          * </pre>
          */
 
-        // Let ftd join the network.
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
-        // Infra device sends an echo request to FTD's OMR.
         mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
 
         // Infra device receives an echo reply sent by FTD.
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, null /* srcAddress */));
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+    }
+
+    @Test
+    public void unicastRouting_afterFactoryResetInfraDevicePingThreadDeviceOmr_replyReceived()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        startInfraDeviceAndWaitForOnLinkAddr();
+        FullThreadDevice ftd = mFtds.get(0);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+
+        mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+    }
+
+    @Test
+    public void unicastRouting_afterInfraNetworkSwitchInfraDevicePingThreadDeviceOmr_replyReceived()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        FullThreadDevice ftd = mFtds.get(0);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+        Inet6Address ftdOmr = ftd.getOmrAddress();
+        // Create a new infra network and let Thread prefer it
+        TestNetworkTracker oldInfraNetworkTracker = mInfraNetworkTracker;
+        try {
+            setUpInfraNetwork();
+            mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+            startInfraDeviceAndWaitForOnLinkAddr();
+
+            mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+            assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+        } finally {
+            runAsShell(MANAGE_TEST_NETWORKS, () -> oldInfraNetworkTracker.teardown());
+        }
     }
 
     @Test
     public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived()
             throws Exception {
-        assumeTrue(isSimulatedThreadRadioSupported());
-
         /*
          * <pre>
          * Topology:
@@ -207,19 +239,10 @@
          * </pre>
          */
 
-        // BR forms a network.
-        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
-        joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
-
-        // Creates a Full Thread Device (FTD) and lets it join the network.
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
-        Inet6Address ftdOmr = ftd.getOmrAddress();
-        Inet6Address ftdMlEid = ftd.getMlEid();
-        assertNotNull(ftdMlEid);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+        Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress());
+        Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid());
 
         ftd.udpBind(ftdOmr, 12345);
         sendUdpMessage(ftdOmr, 12345, "aaaaaaaa");
@@ -231,9 +254,24 @@
     }
 
     @Test
+    public void unicastRouting_meshLocalAddressesAreNotPreferred() throws Exception {
+        // When BR is enabled, there will be OMR address, so the mesh-local addresses are expected
+        // to be deprecated.
+        List<LinkAddress> linkAddresses = getIpv6LinkAddresses("thread-wpan");
+        IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
+
+        for (LinkAddress address : linkAddresses) {
+            if (meshLocalPrefix.contains(address.getAddress())) {
+                assertThat(address.getDeprecationTime()).isAtMost(SystemClock.elapsedRealtime());
+                assertThat(address.isPreferred()).isFalse();
+            }
+        }
+    }
+
+    @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -244,7 +282,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
 
@@ -252,10 +290,10 @@
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void
             multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
                     throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -266,7 +304,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
 
@@ -274,9 +312,9 @@
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -287,18 +325,18 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_inboundForwarding_afterBrRejoinFtdRepliesSubscribedAddress()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
 
         // TODO (b/327311034): Testing bbr state switch from primary mode to secondary mode and back
         // to primary mode requires an additional BR in the Thread network. This is not currently
@@ -306,9 +344,9 @@
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -319,18 +357,18 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
 
-        assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+        assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -341,17 +379,17 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
 
-        assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+        assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -366,28 +404,30 @@
          */
 
         FullThreadDevice ftd1 = mFtds.get(0);
-        startFtdChild(ftd1);
+        joinNetworkAndWaitForOmr(ftd1, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
 
         FullThreadDevice ftd2 = mFtds.get(1);
-        startFtdChild(ftd2);
+        joinNetworkAndWaitForOmr(ftd2, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+        assertNotNull(
+                pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
 
         // Verify ping reply from ftd1 and ftd2 separately as the order of replies can't be
         // predicted.
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+        assertNotNull(
+                pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -402,26 +442,28 @@
          */
 
         FullThreadDevice ftd1 = mFtds.get(0);
-        startFtdChild(ftd1);
+        joinNetworkAndWaitForOmr(ftd1, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
 
         FullThreadDevice ftd2 = mFtds.get(1);
-        startFtdChild(ftd2);
+        joinNetworkAndWaitForOmr(ftd2, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+        assertNotNull(
+                pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
 
         // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+        assertNotNull(
+                pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -432,22 +474,24 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         Inet6Address ftdOmr = ftd.getOmrAddress();
 
         ftd.ping(GROUP_ADDR_SCOPE_5);
         ftd.ping(GROUP_ADDR_SCOPE_4);
 
         assertNotNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+                pollForIcmpPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
         assertNotNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+                pollForIcmpPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -458,18 +502,18 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
 
         ftd.ping(GROUP_ADDR_SCOPE_3);
 
         assertNull(
-                pollForPacketOnInfraNetwork(
+                pollForIcmpPacketOnInfraNetwork(
                         ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -480,19 +524,20 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         Inet6Address ftdLla = ftd.getLinkLocalAddress();
         assertNotNull(ftdLla);
 
-        ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla, 100 /* size */, 1 /* count */);
+        ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla);
 
         assertNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+                pollForIcmpPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_outboundForwarding_mlaToScope4IsNotForwarded() throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -503,23 +548,23 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
         assertFalse(ftdMlas.isEmpty());
 
         for (Inet6Address ftdMla : ftdMlas) {
-            ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla, 100 /* size */, 1 /* count */);
+            ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
 
             assertNull(
-                    pollForPacketOnInfraNetwork(
+                    pollForIcmpPacketOnInfraNetwork(
                             ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
         }
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
             throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -530,7 +575,7 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
         Inet6Address ftdOmr = ftd.getOmrAddress();
 
@@ -538,16 +583,16 @@
         tearDownInfraNetwork();
         setUpInfraNetwork();
         mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
-        startInfraDevice();
+        startInfraDeviceAndWaitForOnLinkAddr();
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
-        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
     }
 
     @Test
+    @RequiresIpv6MulticastRouting
     public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
-        assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
         /*
          * <pre>
          * Topology:
@@ -558,66 +603,59 @@
          */
 
         FullThreadDevice ftd = mFtds.get(0);
-        startFtdChild(ftd);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
         Inet6Address ftdOmr = ftd.getOmrAddress();
 
         // Destroy infra link and re-create
         tearDownInfraNetwork();
         setUpInfraNetwork();
         mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
-        startInfraDevice();
+        startInfraDeviceAndWaitForOnLinkAddr();
 
-        ftd.ping(GROUP_ADDR_SCOPE_5);
         ftd.ping(GROUP_ADDR_SCOPE_4);
 
         assertNotNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
-        assertNotNull(
-                pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+                pollForIcmpPacketOnInfraNetwork(
+                        ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
     }
 
-    private void setUpInfraNetwork() {
+    @Test
+    public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded() throws Exception {
+        FullThreadDevice ftd = mFtds.get(0);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+        // TODO: enable NAT64 via ThreadNetworkController API instead of ot-ctl
+        mOtCtl.setNat64Cidr(NAT64_CIDR);
+        mOtCtl.setNat64Enabled(true);
+        waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), Duration.ofSeconds(10));
+
+        ftd.ping(IPV4_SERVER_ADDR);
+
+        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, IPV4_SERVER_ADDR));
+    }
+
+    private void setUpInfraNetwork() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        // NAT64 feature requires the infra network to have an IPv4 default route.
+        lp.addRoute(
+                new RouteInfo(
+                        new IpPrefix("0.0.0.0/0") /* destination */,
+                        null /* gateway */,
+                        null,
+                        RouteInfo.RTN_UNICAST,
+                        1500 /* mtu */));
         mInfraNetworkTracker =
                 runAsShell(
                         MANAGE_TEST_NETWORKS,
-                        () ->
-                                initTestNetwork(
-                                        mContext, new LinkProperties(), 5000 /* timeoutMs */));
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                NETWORK_SETTINGS,
-                () -> {
-                    CompletableFuture<Void> future = new CompletableFuture<>();
-                    mController.setTestNetworkAsUpstream(
-                            mInfraNetworkTracker.getTestIface().getInterfaceName(),
-                            directExecutor(),
-                            future::complete);
-                    future.get(5, TimeUnit.SECONDS);
-                });
+                        () -> initTestNetwork(mContext, lp, 5000 /* timeoutMs */));
+        String infraNetworkName = mInfraNetworkTracker.getTestIface().getInterfaceName();
+        mController.setTestNetworkAsUpstreamAndWait(infraNetworkName);
     }
 
     private void tearDownInfraNetwork() {
         runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
     }
 
-    private void startBrLeader() throws Exception {
-        CompletableFuture<Void> joinFuture = new CompletableFuture<>();
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
-        joinFuture.get(RESTART_JOIN_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
-    }
-
-    private void startFtdChild(FullThreadDevice ftd) throws Exception {
-        ftd.factoryReset();
-        ftd.joinNetwork(DEFAULT_DATASET);
-        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
-        waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
-        Inet6Address ftdOmr = ftd.getOmrAddress();
-        assertNotNull(ftdOmr);
-    }
-
-    private void startInfraDevice() throws Exception {
+    private void startInfraDeviceAndWaitForOnLinkAddr() throws Exception {
         mInfraDevice =
                 new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
         mInfraDevice.runSlaac(Duration.ofSeconds(60));
@@ -647,20 +685,28 @@
         assertInfraLinkMemberOfGroup(address);
     }
 
-    private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
-        return pollForPacketOnInfraNetwork(type, srcAddress, null);
+    private byte[] pollForIcmpPacketOnInfraNetwork(int type, InetAddress srcAddress) {
+        return pollForIcmpPacketOnInfraNetwork(type, srcAddress, null /* destAddress */);
     }
 
-    private byte[] pollForPacketOnInfraNetwork(
-            int type, Inet6Address srcAddress, Inet6Address destAddress) {
-        Predicate<byte[]> filter;
-        filter =
+    private byte[] pollForIcmpPacketOnInfraNetwork(
+            int type, InetAddress srcAddress, InetAddress destAddress) {
+        if (srcAddress == null && destAddress == null) {
+            throw new IllegalArgumentException("srcAddress and destAddress cannot be both null");
+        }
+        if (srcAddress != null && destAddress != null) {
+            if ((srcAddress instanceof Inet4Address) != (destAddress instanceof Inet4Address)) {
+                throw new IllegalArgumentException(
+                        "srcAddress and destAddress must be both IPv4 or both IPv6");
+            }
+        }
+        boolean isIpv4 =
+                (srcAddress instanceof Inet4Address) || (destAddress instanceof Inet4Address);
+        final Predicate<byte[]> filter =
                 p ->
-                        (isExpectedIcmpv6Packet(p, type)
-                                && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
-                                && (destAddress == null
-                                        ? true
-                                        : isToIpv6Destination(p, destAddress)));
+                        (isIpv4 ? isExpectedIcmpv4Packet(p, type) : isExpectedIcmpv6Packet(p, type))
+                                && (srcAddress == null || isFrom(p, srcAddress))
+                                && (destAddress == null || isTo(p, destAddress));
         return pollForPacket(mInfraNetworkReader, filter);
     }
 }
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
new file mode 100644
index 0000000..2afca5f
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
+import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
+import static android.net.thread.utils.IntegrationTestUtils.discoverService;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
+import static android.net.thread.utils.IntegrationTestUtils.resolveService;
+import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.TapTestNetworkTracker;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Correspondence;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/** Integration test cases for Service Discovery feature. */
+@RunWith(AndroidJUnit4.class)
+@RequiresThreadFeature
+@RequiresSimulationThreadDevice
+@LargeTest
+public class ServiceDiscoveryTest {
+    private static final String TAG = ServiceDiscoveryTest.class.getSimpleName();
+    private static final int NUM_FTD = 3;
+
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+    private static final byte[] DEFAULT_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    private static final ActiveOperationalDataset DEFAULT_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+    private static final Correspondence<byte[], byte[]> BYTE_ARRAY_EQUALITY =
+            Correspondence.from(Arrays::equals, "is equivalent to");
+
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final ThreadNetworkControllerWrapper mController =
+            ThreadNetworkControllerWrapper.newInstance(mContext);
+    private final OtDaemonController mOtCtl = new OtDaemonController();
+    private HandlerThread mHandlerThread;
+    private NsdManager mNsdManager;
+    private TapTestNetworkTracker mTestNetworkTracker;
+    private final List<FullThreadDevice> mFtds = new ArrayList<>();
+    private final List<RegistrationListener> mRegistrationListeners = new ArrayList<>();
+
+    @Before
+    public void setUp() throws Exception {
+        mOtCtl.factoryReset();
+        mController.setEnabledAndWait(true);
+        mController.joinAndWait(DEFAULT_DATASET);
+        mNsdManager = mContext.getSystemService(NsdManager.class);
+
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+
+        mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
+        assertThat(mTestNetworkTracker).isNotNull();
+        mController.setTestNetworkAsUpstreamAndWait(mTestNetworkTracker.getInterfaceName());
+
+        // Create the FTDs in setUp() so that the FTDs can be safely released in tearDown().
+        // Don't create new FTDs in test cases.
+        for (int i = 0; i < NUM_FTD; ++i) {
+            FullThreadDevice ftd = new FullThreadDevice(10 + i /* node ID */);
+            ftd.autoStartSrpClient();
+            mFtds.add(ftd);
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        for (RegistrationListener listener : mRegistrationListeners) {
+            unregisterService(listener);
+        }
+        for (FullThreadDevice ftd : mFtds) {
+            // Clear registered SRP hosts and services
+            if (ftd.isSrpHostRegistered()) {
+                ftd.removeSrpHost();
+            }
+            ftd.destroy();
+        }
+        if (mTestNetworkTracker != null) {
+            mTestNetworkTracker.tearDown();
+        }
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+        mController.setTestNetworkAsUpstreamAndWait(null);
+        mController.leaveAndWait();
+    }
+
+    @Test
+    public void advertisingProxy_multipleSrpClientsRegisterServices_servicesResolvableByMdns()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                    Thread
+         *  Border Router -------------- Full Thread device 1
+         *  (Cuttlefish)         |
+         *                       +------ Full Thread device 2
+         *                       |
+         *                       +------ Full Thread device 3
+         * </pre>
+         */
+
+        // Creates Full Thread Devices (FTD) and let them join the network.
+        for (FullThreadDevice ftd : mFtds) {
+            joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+        }
+
+        int randomId = new Random().nextInt(10_000);
+
+        String serviceNamePrefix = "service-" + randomId + "-";
+        String serviceTypePrefix = "_test" + randomId;
+        String hostnamePrefix = "host-" + randomId + "-";
+
+        // For every FTD, let it register an SRP service.
+        for (int i = 0; i < mFtds.size(); ++i) {
+            FullThreadDevice ftd = mFtds.get(i);
+            ftd.setSrpHostname(hostnamePrefix + i);
+            ftd.setSrpHostAddresses(List.of(ftd.getOmrAddress(), ftd.getMlEid()));
+            ftd.addSrpService(
+                    serviceNamePrefix + i,
+                    serviceTypePrefix + i + "._tcp",
+                    List.of("_sub1", "_sub2"),
+                    12345 /* port */,
+                    Map.of("key1", bytes(0x01, 0x02), "key2", bytes(i)));
+        }
+
+        // Check the advertised services are discoverable and resolvable by NsdManager
+        for (int i = 0; i < mFtds.size(); ++i) {
+            NsdServiceInfo discoveredService =
+                    discoverService(mNsdManager, serviceTypePrefix + i + "._tcp");
+            assertThat(discoveredService).isNotNull();
+            NsdServiceInfo resolvedService = resolveService(mNsdManager, discoveredService);
+            assertThat(resolvedService.getServiceName()).isEqualTo(serviceNamePrefix + i);
+            assertThat(resolvedService.getServiceType()).isEqualTo(serviceTypePrefix + i + "._tcp");
+            assertThat(resolvedService.getPort()).isEqualTo(12345);
+            assertThat(resolvedService.getAttributes())
+                    .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+                    .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(i));
+            assertThat(resolvedService.getHostname()).isEqualTo(hostnamePrefix + i);
+            assertThat(resolvedService.getHostAddresses())
+                    .containsExactly(mFtds.get(i).getOmrAddress());
+        }
+    }
+
+    @Test
+    public void advertisingProxy_srpClientUpdatesService_updatedServiceResolvableByMdns()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                    Thread
+         *  Border Router -------------- Full Thread device
+         *  (Cuttlefish)
+         * </pre>
+         */
+
+        // Creates a Full Thread Devices (FTD) and let it join the network.
+        FullThreadDevice ftd = mFtds.get(0);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+        ftd.setSrpHostname("my-host");
+        ftd.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001:db8::1")));
+        ftd.addSrpService(
+                "my-service",
+                "_test._tcp",
+                Collections.emptyList() /* subtypes */,
+                12345 /* port */,
+                Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+
+        // Update the host addresses
+        ftd.setSrpHostAddresses(
+                List.of(
+                        (Inet6Address) parseNumericAddress("2001:db8::1"),
+                        (Inet6Address) parseNumericAddress("2001:db8::2")));
+        // Update the service
+        ftd.updateSrpService(
+                "my-service", "_test._tcp", List.of("_sub3"), 11111, Map.of("key1", bytes(0x04)));
+        waitFor(ftd::isSrpHostRegistered, SERVICE_DISCOVERY_TIMEOUT);
+
+        // Check the advertised service is discoverable and resolvable by NsdManager
+        NsdServiceInfo discoveredService = discoverService(mNsdManager, "_test._tcp");
+        assertThat(discoveredService).isNotNull();
+        NsdServiceInfo resolvedService =
+                resolveServiceUntil(
+                        mNsdManager,
+                        discoveredService,
+                        s -> s.getPort() == 11111 && s.getHostAddresses().size() == 2);
+        assertThat(resolvedService.getServiceName()).isEqualTo("my-service");
+        assertThat(resolvedService.getServiceType()).isEqualTo("_test._tcp");
+        assertThat(resolvedService.getPort()).isEqualTo(11111);
+        assertThat(resolvedService.getAttributes())
+                .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+                .containsExactly("key1", bytes(0x04));
+        assertThat(resolvedService.getHostname()).isEqualTo("my-host");
+        assertThat(resolvedService.getHostAddresses())
+                .containsExactly(
+                        parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"));
+    }
+
+    @Test
+    @Ignore("TODO: b/333806992 - Enable when it's not flaky at all")
+    public void advertisingProxy_srpClientUnregistersService_serviceIsNotDiscoverableByMdns()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                    Thread
+         *  Border Router -------------- Full Thread device
+         *  (Cuttlefish)
+         * </pre>
+         */
+
+        // Creates a Full Thread Devices (FTD) and let it join the network.
+        FullThreadDevice ftd = mFtds.get(0);
+        joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+        ftd.setSrpHostname("my-host");
+        ftd.setSrpHostAddresses(
+                List.of(
+                        (Inet6Address) parseNumericAddress("2001:db8::1"),
+                        (Inet6Address) parseNumericAddress("2001:db8::2")));
+        ftd.addSrpService(
+                "my-service",
+                "_test._udp",
+                List.of("_sub1"),
+                12345 /* port */,
+                Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+        // Wait for the service to be discoverable by NsdManager.
+        assertThat(discoverService(mNsdManager, "_test._udp")).isNotNull();
+
+        // Unregister the service.
+        CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
+        NsdManager.DiscoveryListener listener =
+                discoverForServiceLost(mNsdManager, "_test._udp", serviceLostFuture);
+        ftd.removeSrpService("my-service", "_test._udp", true /* notifyServer */);
+
+        // Verify the service becomes lost.
+        try {
+            serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+        } finally {
+            mNsdManager.stopServiceDiscovery(listener);
+        }
+        assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_test._udp"));
+    }
+
+    @Test
+    public void meshcopOverlay_vendorAndModelNameAreSetToOverlayValue() throws Exception {
+        NsdServiceInfo discoveredService = discoverService(mNsdManager, "_meshcop._udp");
+        assertThat(discoveredService).isNotNull();
+        NsdServiceInfo meshcopService = resolveService(mNsdManager, discoveredService);
+
+        Map<String, byte[]> txtMap = meshcopService.getAttributes();
+        assertThat(txtMap.get("vn")).isEqualTo("Android".getBytes(UTF_8));
+        assertThat(txtMap.get("mn")).isEqualTo("Thread Border Router".getBytes(UTF_8));
+    }
+
+    @Test
+    @Ignore("TODO: b/332452386 - Enable this test case when it handles the multi-client case well")
+    public void discoveryProxy_multipleClientsBrowseAndResolveServiceOverMdns() throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                    Thread
+         *  Border Router -------------- Full Thread device
+         *  (Cuttlefish)
+         * </pre>
+         */
+
+        RegistrationListener listener = new RegistrationListener();
+        NsdServiceInfo info = new NsdServiceInfo();
+        info.setServiceType("_testservice._tcp");
+        info.setServiceName("test-service");
+        info.setPort(12345);
+        info.setHostname("testhost");
+        info.setHostAddresses(List.of(parseNumericAddress("2001::1")));
+        info.setAttribute("key1", bytes(0x01, 0x02));
+        info.setAttribute("key2", bytes(0x03));
+        registerService(info, listener);
+        mRegistrationListeners.add(listener);
+        for (int i = 0; i < NUM_FTD; ++i) {
+            FullThreadDevice ftd = mFtds.get(i);
+            joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
+            ftd.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
+        }
+        final ArrayList<NsdServiceInfo> browsedServices = new ArrayList<>();
+        final ArrayList<NsdServiceInfo> resolvedServices = new ArrayList<>();
+        final ArrayList<Thread> threads = new ArrayList<>();
+        for (int i = 0; i < NUM_FTD; ++i) {
+            browsedServices.add(null);
+            resolvedServices.add(null);
+        }
+        for (int i = 0; i < NUM_FTD; ++i) {
+            final FullThreadDevice ftd = mFtds.get(i);
+            final int index = i;
+            Runnable task =
+                    () -> {
+                        browsedServices.set(
+                                index,
+                                ftd.browseService("_testservice._tcp.default.service.arpa."));
+                        resolvedServices.set(
+                                index,
+                                ftd.resolveService(
+                                        "test-service", "_testservice._tcp.default.service.arpa."));
+                    };
+            threads.add(new Thread(task));
+        }
+        for (Thread thread : threads) {
+            thread.start();
+        }
+        for (Thread thread : threads) {
+            thread.join();
+        }
+
+        for (int i = 0; i < NUM_FTD; ++i) {
+            NsdServiceInfo browsedService = browsedServices.get(i);
+            assertThat(browsedService.getServiceName()).isEqualTo("test-service");
+            assertThat(browsedService.getPort()).isEqualTo(12345);
+
+            NsdServiceInfo resolvedService = resolvedServices.get(i);
+            assertThat(resolvedService.getServiceName()).isEqualTo("test-service");
+            assertThat(resolvedService.getPort()).isEqualTo(12345);
+            assertThat(resolvedService.getHostname()).isEqualTo("testhost.default.service.arpa.");
+            assertThat(resolvedService.getHostAddresses())
+                    .containsExactly(parseNumericAddress("2001::1"));
+            assertThat(resolvedService.getAttributes())
+                    .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+                    .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+        }
+    }
+
+    @Test
+    public void discoveryProxy_browseAndResolveServiceAtSrpServer() throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                    Thread
+         *  Border Router -------+------ SRP client
+         *  (Cuttlefish)         |
+         *                       +------ DNS client
+         *
+         * </pre>
+         */
+        FullThreadDevice srpClient = mFtds.get(0);
+        joinNetworkAndWaitForOmr(srpClient, DEFAULT_DATASET);
+        srpClient.setSrpHostname("my-host");
+        srpClient.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001::1")));
+        srpClient.addSrpService(
+                "my-service",
+                "_test._udp",
+                List.of("_sub1"),
+                12345 /* port */,
+                Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+
+        FullThreadDevice dnsClient = mFtds.get(1);
+        joinNetworkAndWaitForOmr(dnsClient, DEFAULT_DATASET);
+        dnsClient.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
+
+        NsdServiceInfo browsedService = dnsClient.browseService("_test._udp.default.service.arpa.");
+        assertThat(browsedService.getServiceName()).isEqualTo("my-service");
+        assertThat(browsedService.getPort()).isEqualTo(12345);
+        assertThat(browsedService.getHostname()).isEqualTo("my-host.default.service.arpa.");
+        assertThat(browsedService.getHostAddresses())
+                .containsExactly(parseNumericAddress("2001::1"));
+        assertThat(browsedService.getAttributes())
+                .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+                .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+
+        NsdServiceInfo resolvedService =
+                dnsClient.resolveService("my-service", "_test._udp.default.service.arpa.");
+        assertThat(resolvedService.getServiceName()).isEqualTo("my-service");
+        assertThat(resolvedService.getPort()).isEqualTo(12345);
+        assertThat(resolvedService.getHostname()).isEqualTo("my-host.default.service.arpa.");
+        assertThat(resolvedService.getHostAddresses())
+                .containsExactly(parseNumericAddress("2001::1"));
+        assertThat(resolvedService.getAttributes())
+                .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+                .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+    }
+
+    private void registerService(NsdServiceInfo serviceInfo, RegistrationListener listener)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, listener);
+        listener.waitForRegistered();
+    }
+
+    private void unregisterService(RegistrationListener listener)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        mNsdManager.unregisterService(listener);
+        listener.waitForUnregistered();
+    }
+
+    private static class RegistrationListener implements NsdManager.RegistrationListener {
+        private final CompletableFuture<Void> mRegisteredFuture = new CompletableFuture<>();
+        private final CompletableFuture<Void> mUnRegisteredFuture = new CompletableFuture<>();
+
+        RegistrationListener() {}
+
+        @Override
+        public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {}
+
+        @Override
+        public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {}
+
+        @Override
+        public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+            mRegisteredFuture.complete(null);
+        }
+
+        @Override
+        public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+            mUnRegisteredFuture.complete(null);
+        }
+
+        public void waitForRegistered()
+                throws InterruptedException, ExecutionException, TimeoutException {
+            mRegisteredFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+        }
+
+        public void waitForUnregistered()
+                throws InterruptedException, ExecutionException, TimeoutException {
+            mUnRegisteredFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+        }
+    }
+
+    private static byte[] bytes(int... byteInts) {
+        byte[] bytes = new byte[byteInts.length];
+        for (int i = 0; i < byteInts.length; ++i) {
+            bytes[i] = (byte) byteInts[i];
+        }
+        return bytes;
+    }
+}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 70897f0..61b6eac 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -16,31 +16,36 @@
 
 package android.net.thread;
 
-import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
-import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.LEAVE_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
+import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.getPrefixesFromNetData;
+import static android.net.thread.utils.IntegrationTestUtils.getThreadNetwork;
+import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
 
 import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static com.google.common.truth.Truth.assertWithMessage;
 
-import static org.junit.Assume.assumeNotNull;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-
-import android.annotation.Nullable;
 import android.content.Context;
-import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.ConnectivityManager;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.thread.utils.FullThreadDevice;
 import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
 import android.os.SystemClock;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -49,20 +54,34 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
 import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.time.Duration;
+import java.util.Arrays;
 import java.util.List;
-import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 /** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
 @LargeTest
+@RequiresThreadFeature
 @RunWith(AndroidJUnit4.class)
 public class ThreadIntegrationTest {
-    private final Context mContext = ApplicationProvider.getApplicationContext();
-    private ThreadNetworkController mController;
-    private OtDaemonController mOtCtl;
+    // The byte[] buffer size for UDP tests
+    private static final int UDP_BUFFER_SIZE = 1024;
+
+    // The maximum time for OT addresses to be propagated to the TUN interface "thread-wpan"
+    private static final Duration TUN_ADDR_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+
+    // The maximum time for changes to be propagated to netdata.
+    private static final Duration NET_DATA_UPDATE_TIMEOUT = Duration.ofSeconds(1);
 
     // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
     private static final byte[] DEFAULT_DATASET_TLVS =
@@ -75,124 +94,276 @@
     private static final ActiveOperationalDataset DEFAULT_DATASET =
             ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
 
+    private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
+            (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
+
+    private static final String TEST_NO_SLAAC_PREFIX = "9101:dead:beef:cafe::/64";
+    private static final InetAddress TEST_NO_SLAAC_PREFIX_ADDRESS =
+            InetAddresses.parseNumericAddress("9101:dead:beef:cafe::");
+
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    private ExecutorService mExecutor;
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final ThreadNetworkControllerWrapper mController =
+            ThreadNetworkControllerWrapper.newInstance(mContext);
+    private OtDaemonController mOtCtl;
+    private FullThreadDevice mFtd;
+
     @Before
     public void setUp() throws Exception {
-        final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
-        if (manager != null) {
-            mController = manager.getAllThreadNetworkControllers().get(0);
-        }
-
-        // Run the tests on only devices where the Thread feature is available
-        assumeNotNull(mController);
-
+        mExecutor = Executors.newSingleThreadExecutor();
         mOtCtl = new OtDaemonController();
-        leaveAndWait(mController);
+        mController.leaveAndWait();
+
+        // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+        mOtCtl.factoryReset();
+
+        mFtd = new FullThreadDevice(10 /* nodeId */);
     }
 
     @After
     public void tearDown() throws Exception {
-        if (mController == null) {
-            return;
-        }
+        mController.setTestNetworkAsUpstreamAndWait(null);
+        mController.leaveAndWait();
 
-        setTestUpStreamNetworkAndWait(mController, null);
-        leaveAndWait(mController);
+        mFtd.destroy();
+        mExecutor.shutdownNow();
     }
 
     @Test
     public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
-        leaveAndWait(mController);
+        mController.leaveAndWait();
 
         runShellCommand("stop ot-daemon");
         // TODO(b/323331973): the sleep is needed to workaround the race conditions
         SystemClock.sleep(200);
 
-        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_STOPPED), CALLBACK_TIMEOUT);
+        mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT);
     }
 
     @Test
-    public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoined() throws Exception {
-        joinAndWait(mController, DEFAULT_DATASET);
+    public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()
+            throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
 
         runShellCommand("stop ot-daemon");
 
-        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_DETACHED), CALLBACK_TIMEOUT);
-        waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+        mController.waitForRole(DEVICE_ROLE_DETACHED, CALLBACK_TIMEOUT);
+        mController.waitForRole(DEVICE_ROLE_LEADER, RESTART_JOIN_TIMEOUT);
+        assertThat(mOtCtl.isInterfaceUp()).isTrue();
+        assertThat(runShellCommand("ifconfig thread-wpan")).contains("UP POINTOPOINT RUNNING");
     }
 
     @Test
     public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
-        joinAndWait(mController, DEFAULT_DATASET);
+        mController.joinAndWait(DEFAULT_DATASET);
 
         mOtCtl.factoryReset();
 
-        assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+        assertThat(mController.getDeviceRole()).isEqualTo(DEVICE_ROLE_STOPPED);
     }
 
     @Test
     public void otDaemonFactoryReset_addressesRemoved() throws Exception {
-        joinAndWait(mController, DEFAULT_DATASET);
+        mController.joinAndWait(DEFAULT_DATASET);
 
         mOtCtl.factoryReset();
-        String ifconfig = runShellCommand("ifconfig thread-wpan");
 
+        String ifconfig = runShellCommand("ifconfig thread-wpan");
         assertThat(ifconfig).doesNotContain("inet6 addr");
     }
 
+    // TODO (b/323300829): add test for removing an OT address
     @Test
-    public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception {
-        joinAndWait(mController, DEFAULT_DATASET);
+    public void tunInterface_joinedNetwork_otAndTunAddressesMatch() throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
 
-        String ifconfig = runShellCommand("ifconfig thread-wpan");
         List<Inet6Address> otAddresses = mOtCtl.getAddresses();
         assertThat(otAddresses).isNotEmpty();
-        for (Inet6Address otAddress : otAddresses) {
-            assertThat(ifconfig).contains(otAddress.getHostAddress());
+        // TODO: it's cleaner to have a retry() method to retry failed asserts in given delay so
+        // that we can write assertThat() in the Predicate
+        waitFor(
+                () -> {
+                    List<Inet6Address> tunAddresses =
+                            getIpv6LinkAddresses("thread-wpan").stream()
+                                    .map(linkAddr -> (Inet6Address) linkAddr.getAddress())
+                                    .toList();
+                    return otAddresses.containsAll(tunAddresses)
+                            && tunAddresses.containsAll(otAddresses);
+                },
+                TUN_ADDR_UPDATE_TIMEOUT);
+    }
+
+    @Test
+    public void otDaemonRestart_latestCountryCodeIsSetToOtDaemon() throws Exception {
+        runThreadCommand("force-country-code enabled CN");
+
+        runShellCommand("stop ot-daemon");
+        // TODO(b/323331973): the sleep is needed to workaround the race conditions
+        SystemClock.sleep(200);
+        mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT);
+
+        assertThat(mOtCtl.getCountryCode()).isEqualTo("CN");
+    }
+
+    @Test
+    public void udp_appStartEchoServer_endDeviceUdpEchoSuccess() throws Exception {
+        // Topology:
+        //   Test App ------ thread-wpan ------ End Device
+
+        mController.joinAndWait(DEFAULT_DATASET);
+        startFtdChild(mFtd, DEFAULT_DATASET);
+        final Inet6Address serverAddress = mOtCtl.getMeshLocalAddresses().get(0);
+        final int serverPort = 9527;
+
+        mExecutor.execute(() -> startUdpEchoServerAndWait(serverAddress, serverPort));
+        mFtd.udpOpen();
+        mFtd.udpSend("Hello,Thread", serverAddress, serverPort);
+        String udpReply = mFtd.udpReceive();
+
+        assertThat(udpReply).isEqualTo("Hello,Thread");
+    }
+
+    @Test
+    public void joinNetworkWithBrDisabled_meshLocalAddressesArePreferred() throws Exception {
+        // When BR feature is disabled, there is no OMR address, so the mesh-local addresses are
+        // expected to be preferred.
+        mOtCtl.executeCommand("br disable");
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
+        List<LinkAddress> linkAddresses = getIpv6LinkAddresses("thread-wpan");
+        for (LinkAddress address : linkAddresses) {
+            if (meshLocalPrefix.contains(address.getAddress())) {
+                assertThat(address.getDeprecationTime())
+                        .isGreaterThan(SystemClock.elapsedRealtime());
+                assertThat(address.isPreferred()).isTrue();
+            }
         }
+
+        mOtCtl.executeCommand("br enable");
+    }
+
+    @Test
+    public void joinNetwork_tunInterfaceJoinsAllRouterMulticastGroup() throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        assertTunInterfaceMemberOfGroup(GROUP_ADDR_ALL_ROUTERS);
+    }
+
+    @Test
+    public void edPingsMeshLocalAddresses_oneReplyPerRequest() throws Exception {
+        mController.joinAndWait(DEFAULT_DATASET);
+        startFtdChild(mFtd, DEFAULT_DATASET);
+        List<Inet6Address> meshLocalAddresses = mOtCtl.getMeshLocalAddresses();
+
+        for (Inet6Address address : meshLocalAddresses) {
+            assertWithMessage(
+                            "There may be duplicated replies of ping request to "
+                                    + address.getHostAddress())
+                    .that(mFtd.ping(address, 2))
+                    .isEqualTo(2);
+        }
+    }
+
+    @Test
+    public void addPrefixToNetData_routeIsAddedToTunInterface() throws Exception {
+        ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        // Ftd child doesn't have the ability to add a prefix, so let BR itself add a prefix.
+        mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
+        mOtCtl.executeCommand("netdata register");
+        waitFor(
+                () -> {
+                    String netData = mOtCtl.executeCommand("netdata show");
+                    return getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
+                },
+                NET_DATA_UPDATE_TIMEOUT);
+
+        LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
+        assertThat(lp).isNotNull();
+        assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
+                .isTrue();
+    }
+
+    @Test
+    public void removePrefixFromNetData_routeIsRemovedFromTunInterface() throws Exception {
+        ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        mController.joinAndWait(DEFAULT_DATASET);
+        mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
+        mOtCtl.executeCommand("netdata register");
+
+        mOtCtl.executeCommand("prefix remove " + TEST_NO_SLAAC_PREFIX);
+        mOtCtl.executeCommand("netdata register");
+        waitFor(
+                () -> {
+                    String netData = mOtCtl.executeCommand("netdata show");
+                    return !getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
+                },
+                NET_DATA_UPDATE_TIMEOUT);
+
+        LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
+        assertThat(lp).isNotNull();
+        assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
+                .isFalse();
+    }
+
+    @Test
+    public void toggleThreadNetwork_routeFromPreviousNetDataIsRemoved() throws Exception {
+        ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        mController.joinAndWait(DEFAULT_DATASET);
+        mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
+        mOtCtl.executeCommand("netdata register");
+
+        mController.leaveAndWait();
+        mOtCtl.factoryReset();
+        mController.joinAndWait(DEFAULT_DATASET);
+
+        LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
+        assertThat(lp).isNotNull();
+        assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
+                .isFalse();
     }
 
     // TODO (b/323300829): add more tests for integration with linux platform and
     // ConnectivityService
 
-    private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
-        CompletableFuture<Integer> future = new CompletableFuture<>();
-        StateCallback callback = future::complete;
-        controller.registerStateCallback(directExecutor(), callback);
-        try {
-            return future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
-        } finally {
-            controller.unregisterStateCallback(callback);
+    private static String runThreadCommand(String cmd) {
+        return runShellCommandOrThrow("cmd thread_network " + cmd);
+    }
+
+    private void startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
+            throws Exception {
+        ftd.factoryReset();
+        ftd.joinNetwork(activeDataset);
+        ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
+    }
+
+    /**
+     * Starts a UDP echo server and replies to the first UDP message.
+     *
+     * <p>This method exits when the first UDP message is received and the reply is sent
+     */
+    private void startUdpEchoServerAndWait(InetAddress serverAddress, int serverPort) {
+        try (var udpServerSocket = new DatagramSocket(serverPort, serverAddress)) {
+            DatagramPacket recvPacket =
+                    new DatagramPacket(new byte[UDP_BUFFER_SIZE], UDP_BUFFER_SIZE);
+            udpServerSocket.receive(recvPacket);
+            byte[] sendBuffer = Arrays.copyOf(recvPacket.getData(), recvPacket.getData().length);
+            udpServerSocket.send(
+                    new DatagramPacket(
+                            sendBuffer,
+                            sendBuffer.length,
+                            (Inet6Address) recvPacket.getAddress(),
+                            recvPacket.getPort()));
+        } catch (IOException e) {
+            throw new IllegalStateException(e);
         }
     }
 
-    private static void joinAndWait(
-            ThreadNetworkController controller, ActiveOperationalDataset activeDataset)
-            throws Exception {
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                () -> controller.join(activeDataset, directExecutor(), result -> {}));
-        waitForStateAnyOf(controller, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
-    }
-
-    private static void leaveAndWait(ThreadNetworkController controller) throws Exception {
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                () -> controller.leave(directExecutor(), future::complete));
-        future.get(LEAVE_TIMEOUT.toMillis(), MILLISECONDS);
-    }
-
-    private static void setTestUpStreamNetworkAndWait(
-            ThreadNetworkController controller, @Nullable String networkInterfaceName)
-            throws Exception {
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        runAsShell(
-                PERMISSION_THREAD_NETWORK_PRIVILEGED,
-                NETWORK_SETTINGS,
-                () -> {
-                    controller.setTestNetworkAsUpstream(
-                            networkInterfaceName, directExecutor(), future::complete);
-                });
-        future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
+    private void assertTunInterfaceMemberOfGroup(Inet6Address address) throws Exception {
+        waitFor(() -> isInMulticastGroup(TUN_IF_NAME, address), TUN_ADDR_UPDATE_TIMEOUT);
     }
 }
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
new file mode 100644
index 0000000..ba04348
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.os.OutcomeReceiver;
+import android.util.SparseIntArray;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** Tests for hide methods of {@link ThreadNetworkController}. */
+@LargeTest
+@RequiresThreadFeature
+@RunWith(AndroidJUnit4.class)
+public class ThreadNetworkControllerTest {
+    private static final int VALID_POWER = 32_767;
+    private static final int INVALID_POWER = 32_768;
+    private static final int VALID_CHANNEL = 20;
+    private static final int INVALID_CHANNEL = 10;
+    private static final String THREAD_NETWORK_PRIVILEGED =
+            "android.permission.THREAD_NETWORK_PRIVILEGED";
+
+    private static final SparseIntArray CHANNEL_MAX_POWERS =
+            new SparseIntArray() {
+                {
+                    put(20, 32767);
+                }
+            };
+
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private ExecutorService mExecutor;
+    private ThreadNetworkController mController;
+
+    @Before
+    public void setUp() throws Exception {
+        mController =
+                mContext.getSystemService(ThreadNetworkManager.class)
+                        .getAllThreadNetworkControllers()
+                        .get(0);
+
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        dropAllPermissions();
+    }
+
+    @Test
+    public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
+        CompletableFuture<Void> powerFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        mController.setChannelMaxPowers(
+                                CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
+
+        try {
+            assertThat(powerFuture.get()).isNull();
+        } catch (ExecutionException exception) {
+            ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
+            assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_OPERATION);
+        }
+    }
+
+    @Test
+    public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
+    }
+
+    @Test
+    public void setChannelMaxPowers_emptyChannelMaxPower_throwsIllegalArgumentException() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
+    }
+
+    @Test
+    public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
+        final SparseIntArray INVALID_CHANNEL_ARRAY =
+                new SparseIntArray() {
+                    {
+                        put(INVALID_CHANNEL, VALID_POWER);
+                    }
+                };
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
+    }
+
+    @Test
+    public void setChannelMaxPowers_invalidPower_throwsIllegalArgumentException() {
+        final SparseIntArray INVALID_POWER_ARRAY =
+                new SparseIntArray() {
+                    {
+                        put(VALID_CHANNEL, INVALID_POWER);
+                    }
+                };
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.setChannelMaxPowers(INVALID_POWER_ARRAY, mExecutor, v -> {}));
+    }
+
+    private static void dropAllPermissions() {
+        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+            CompletableFuture<V> future) {
+        return new OutcomeReceiver<V, ThreadNetworkException>() {
+            @Override
+            public void onResult(V result) {
+                future.complete(result);
+            }
+
+            @Override
+            public void onError(ThreadNetworkException e) {
+                future.completeExceptionally(e);
+            }
+        };
+    }
+}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
new file mode 100644
index 0000000..8835f40
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.ExecutionException;
+
+/** Integration tests for {@link ThreadNetworkShellCommand}. */
+@LargeTest
+@RequiresThreadFeature
+@RunWith(AndroidJUnit4.class)
+public class ThreadNetworkShellCommandTest {
+    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final ThreadNetworkControllerWrapper mController =
+            ThreadNetworkControllerWrapper.newInstance(mContext);
+
+    @Before
+    public void setUp() {
+        ensureThreadEnabled();
+    }
+
+    @After
+    public void tearDown() {
+        ensureThreadEnabled();
+    }
+
+    private static void ensureThreadEnabled() {
+        runThreadCommand("force-stop-ot-daemon disabled");
+        runThreadCommand("enable");
+    }
+
+    @Test
+    public void enable_threadStateIsEnabled() throws Exception {
+        runThreadCommand("enable");
+
+        assertThat(mController.getEnabledState()).isEqualTo(STATE_ENABLED);
+    }
+
+    @Test
+    public void disable_threadStateIsDisabled() throws Exception {
+        runThreadCommand("disable");
+
+        assertThat(mController.getEnabledState()).isEqualTo(STATE_DISABLED);
+    }
+
+    @Test
+    public void forceStopOtDaemon_forceStopEnabled_otDaemonServiceDisappear() {
+        runThreadCommand("force-stop-ot-daemon enabled");
+
+        assertThat(runShellCommandOrThrow("service list")).doesNotContain("ot_daemon");
+    }
+
+    @Test
+    public void forceStopOtDaemon_forceStopEnabled_canNotEnableThread() throws Exception {
+        runThreadCommand("force-stop-ot-daemon enabled");
+
+        ExecutionException thrown =
+                assertThrows(ExecutionException.class, () -> mController.setEnabledAndWait(true));
+        ThreadNetworkException cause = (ThreadNetworkException) thrown.getCause();
+        assertThat(cause.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+    }
+
+    @Test
+    public void forceStopOtDaemon_forceStopDisabled_otDaemonServiceAppears() throws Exception {
+        runThreadCommand("force-stop-ot-daemon disabled");
+
+        assertThat(runShellCommandOrThrow("service list")).contains("ot_daemon");
+    }
+
+    @Test
+    public void forceStopOtDaemon_forceStopDisabled_canEnableThread() throws Exception {
+        runThreadCommand("force-stop-ot-daemon disabled");
+
+        mController.setEnabledAndWait(true);
+        assertThat(mController.getEnabledState()).isEqualTo(STATE_ENABLED);
+    }
+
+    @Test
+    public void forceCountryCode_setCN_getCountryCodeReturnsCN() {
+        runThreadCommand("force-country-code enabled CN");
+
+        final String result = runThreadCommand("get-country-code");
+        assertThat(result).contains("Thread country code = CN");
+    }
+
+    private static String runThreadCommand(String cmd) {
+        return runShellCommandOrThrow("cmd thread_network " + cmd);
+    }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 6cb1675..083a841 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -15,15 +15,21 @@
  */
 package android.net.thread.utils;
 
+import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
 
 import static com.google.common.io.BaseEncoding.base16;
 
-import static org.junit.Assert.fail;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import android.net.InetAddresses;
 import android.net.IpPrefix;
+import android.net.nsd.NsdServiceInfo;
 import android.net.thread.ActiveOperationalDataset;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.google.errorprone.annotations.FormatMethod;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
@@ -31,9 +37,14 @@
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -47,9 +58,21 @@
  * available commands.
  */
 public final class FullThreadDevice {
+    private static final int HOP_LIMIT = 64;
+    private static final int PING_INTERVAL = 1;
+    private static final int PING_SIZE = 100;
+    // There may not be a response for the ping command, using a short timeout to keep the tests
+    // short.
+    private static final float PING_TIMEOUT_0_1_SECOND = 0.1f;
+    // 1 second timeout should be used when response is expected.
+    private static final float PING_TIMEOUT_1_SECOND = 1f;
+    private static final int READ_LINE_TIMEOUT_SECONDS = 5;
+
     private final Process mProcess;
     private final BufferedReader mReader;
     private final BufferedWriter mWriter;
+    private final HandlerThread mReaderHandlerThread;
+    private final Handler mReaderHandler;
 
     private ActiveOperationalDataset mActiveOperationalDataset;
 
@@ -66,17 +89,22 @@
      */
     public FullThreadDevice(int nodeId) {
         try {
-            mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd " + nodeId);
+            mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd -Leth1 " + nodeId);
         } catch (IOException e) {
-            throw new IllegalStateException("Failed to start ot-cli-ftd (id=" + nodeId + ")", e);
+            throw new IllegalStateException(
+                    "Failed to start ot-cli-ftd -Leth1 (id=" + nodeId + ")", e);
         }
         mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
         mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
+        mReaderHandlerThread = new HandlerThread("FullThreadDeviceReader");
+        mReaderHandlerThread.start();
+        mReaderHandler = new Handler(mReaderHandlerThread.getLooper());
         mActiveOperationalDataset = null;
     }
 
     public void destroy() {
         mProcess.destroy();
+        mReaderHandlerThread.quit();
     }
 
     /**
@@ -191,19 +219,185 @@
     public void udpBind(Inet6Address address, int port) {
         udpClose();
         udpOpen();
-        executeCommand(String.format("udp bind %s %d", address.getHostAddress(), port));
+        executeCommand("udp bind %s %d", address.getHostAddress(), port);
     }
 
     /** Returns the message received on the UDP socket. */
     public String udpReceive() throws IOException {
         Pattern pattern =
                 Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
-        Matcher matcher = pattern.matcher(mReader.readLine());
+        Matcher matcher = pattern.matcher(readLine());
         matcher.matches();
 
         return matcher.group(4);
     }
 
+    /** Sends a UDP message to given IPv6 address and port. */
+    public void udpSend(String message, Inet6Address serverAddr, int serverPort) {
+        executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
+    }
+
+    /** Enables the SRP client and run in autostart mode. */
+    public void autoStartSrpClient() {
+        executeCommand("srp client autostart enable");
+    }
+
+    /** Sets the hostname (e.g. "MyHost") for the SRP client. */
+    public void setSrpHostname(String hostname) {
+        executeCommand("srp client host name " + hostname);
+    }
+
+    /** Sets the host addresses for the SRP client. */
+    public void setSrpHostAddresses(List<Inet6Address> addresses) {
+        executeCommand(
+                "srp client host address "
+                        + String.join(
+                                " ",
+                                addresses.stream().map(Inet6Address::getHostAddress).toList()));
+    }
+
+    /** Removes the SRP host */
+    public void removeSrpHost() {
+        executeCommand("srp client host remove 1 1");
+    }
+
+    /**
+     * Adds an SRP service for the SRP client and wait for the registration to complete.
+     *
+     * @param serviceName the service name like "MyService"
+     * @param serviceType the service type like "_test._tcp"
+     * @param subtypes the service subtypes like "_sub1"
+     * @param port the port number in range [1, 65535]
+     * @param txtMap the map of TXT names and values
+     * @throws TimeoutException if the service isn't registered within timeout
+     */
+    public void addSrpService(
+            String serviceName,
+            String serviceType,
+            List<String> subtypes,
+            int port,
+            Map<String, byte[]> txtMap)
+            throws TimeoutException {
+        StringBuilder fullServiceType = new StringBuilder(serviceType);
+        for (String subtype : subtypes) {
+            fullServiceType.append(",").append(subtype);
+        }
+        waitForSrpServer();
+        executeCommand(
+                "srp client service add %s %s %d %d %d %s",
+                serviceName,
+                fullServiceType,
+                port,
+                0 /* priority */,
+                0 /* weight */,
+                txtMapToHexString(txtMap));
+        waitFor(() -> isSrpServiceRegistered(serviceName, serviceType), SERVICE_DISCOVERY_TIMEOUT);
+    }
+
+    /**
+     * Removes an SRP service for the SRP client.
+     *
+     * @param serviceName the service name like "MyService"
+     * @param serviceType the service type like "_test._tcp"
+     * @param notifyServer whether to notify SRP server about the removal
+     */
+    public void removeSrpService(String serviceName, String serviceType, boolean notifyServer) {
+        String verb = notifyServer ? "remove" : "clear";
+        executeCommand("srp client service %s %s %s", verb, serviceName, serviceType);
+    }
+
+    /**
+     * Updates an existing SRP service for the SRP client.
+     *
+     * <p>This is essentially a 'remove' and an 'add' on the SRP client's side.
+     *
+     * @param serviceName the service name like "MyService"
+     * @param serviceType the service type like "_test._tcp"
+     * @param subtypes the service subtypes like "_sub1"
+     * @param port the port number in range [1, 65535]
+     * @param txtMap the map of TXT names and values
+     * @throws TimeoutException if the service isn't updated within timeout
+     */
+    public void updateSrpService(
+            String serviceName,
+            String serviceType,
+            List<String> subtypes,
+            int port,
+            Map<String, byte[]> txtMap)
+            throws TimeoutException {
+        removeSrpService(serviceName, serviceType, false /* notifyServer */);
+        addSrpService(serviceName, serviceType, subtypes, port, txtMap);
+    }
+
+    /** Checks if an SRP service is registered. */
+    public boolean isSrpServiceRegistered(String serviceName, String serviceType) {
+        List<String> lines = executeCommand("srp client service");
+        for (String line : lines) {
+            if (line.contains(serviceName) && line.contains(serviceType)) {
+                return line.contains("Registered");
+            }
+        }
+        return false;
+    }
+
+    /** Checks if an SRP host is registered. */
+    public boolean isSrpHostRegistered() {
+        List<String> lines = executeCommand("srp client host");
+        for (String line : lines) {
+            return line.contains("Registered");
+        }
+        return false;
+    }
+
+    /** Sets the DNS server address. */
+    public void setDnsServerAddress(String address) {
+        executeCommand("dns config " + address);
+    }
+
+    /** Returns the first browsed service instance of {@code serviceType}. */
+    public NsdServiceInfo browseService(String serviceType) {
+        // CLI output:
+        // DNS browse response for _testservice._tcp.default.service.arpa.
+        // test-service
+        //    Port:12345, Priority:0, Weight:0, TTL:10
+        //    Host:testhost.default.service.arpa.
+        //    HostAddress:2001:0:0:0:0:0:0:1 TTL:10
+        //    TXT:[key1=0102, key2=03] TTL:10
+
+        List<String> lines = executeCommand("dns browse " + serviceType);
+        NsdServiceInfo info = new NsdServiceInfo();
+        info.setServiceName(lines.get(1));
+        info.setServiceType(serviceType);
+        info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(2)));
+        info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(3)));
+        info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(4))));
+        DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(5), info);
+
+        return info;
+    }
+
+    /** Returns the resolved service instance. */
+    public NsdServiceInfo resolveService(String serviceName, String serviceType) {
+        // CLI output:
+        // DNS service resolution response for test-service for service
+        // _test._tcp.default.service.arpa.
+        // Port:12345, Priority:0, Weight:0, TTL:10
+        // Host:Android.default.service.arpa.
+        // HostAddress:2001:0:0:0:0:0:0:1 TTL:10
+        // TXT:[key1=0102, key2=03] TTL:10
+
+        List<String> lines = executeCommand("dns service %s %s", serviceName, serviceType);
+        NsdServiceInfo info = new NsdServiceInfo();
+        info.setServiceName(serviceName);
+        info.setServiceType(serviceType);
+        info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(1)));
+        info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(2)));
+        info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(3))));
+        DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(4), info);
+
+        return info;
+    }
+
     /** Runs the "factoryreset" command on the device. */
     public void factoryReset() {
         try {
@@ -223,7 +417,50 @@
         executeCommand("ipmaddr add " + address.getHostAddress());
     }
 
-    public void ping(Inet6Address address, Inet6Address source, int size, int count) {
+    public void ping(InetAddress address, Inet6Address source) {
+        ping(
+                address,
+                source,
+                PING_SIZE,
+                1 /* count */,
+                PING_INTERVAL,
+                HOP_LIMIT,
+                PING_TIMEOUT_0_1_SECOND);
+    }
+
+    public void ping(InetAddress address) {
+        ping(
+                address,
+                null,
+                PING_SIZE,
+                1 /* count */,
+                PING_INTERVAL,
+                HOP_LIMIT,
+                PING_TIMEOUT_0_1_SECOND);
+    }
+
+    /** Returns the number of ping reply packets received. */
+    public int ping(InetAddress address, int count) {
+        List<String> output =
+                ping(
+                        address,
+                        null,
+                        PING_SIZE,
+                        count,
+                        PING_INTERVAL,
+                        HOP_LIMIT,
+                        PING_TIMEOUT_1_SECOND);
+        return getReceivedPacketsCount(output);
+    }
+
+    private List<String> ping(
+            InetAddress address,
+            Inet6Address source,
+            int size,
+            int count,
+            int interval,
+            int hopLimit,
+            float timeout) {
         String cmd =
                 "ping"
                         + ((source == null) ? "" : (" -I " + source.getHostAddress()))
@@ -232,12 +469,49 @@
                         + " "
                         + size
                         + " "
-                        + count;
-        executeCommand(cmd);
+                        + count
+                        + " "
+                        + interval
+                        + " "
+                        + hopLimit
+                        + " "
+                        + timeout;
+        return executeCommand(cmd);
     }
 
-    public void ping(Inet6Address address) {
-        ping(address, null, 100 /* size */, 1 /* count */);
+    private int getReceivedPacketsCount(List<String> stringList) {
+        Pattern pattern = Pattern.compile("([\\d]+) packets received");
+
+        for (String message : stringList) {
+            Matcher matcher = pattern.matcher(message);
+            if (matcher.find()) {
+                String packetCountStr = matcher.group(1);
+                return Integer.parseInt(packetCountStr);
+            }
+        }
+        // No match found
+        return -1;
+    }
+
+    /** Waits for an SRP server to be present in Network Data */
+    private void waitForSrpServer() throws TimeoutException {
+        // CLI output:
+        // > srp client server
+        // [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538
+        // Done
+        waitFor(
+                () -> {
+                    final String serverAddr = executeCommand("srp client server").get(0);
+                    final int lastColonIndex = serverAddr.lastIndexOf(':');
+                    final int port = Integer.parseInt(serverAddr.substring(lastColonIndex + 1));
+                    return port > 0;
+                },
+                SERVICE_DISCOVERY_TIMEOUT);
+    }
+
+    @FormatMethod
+    private List<String> executeCommand(String commandFormat, Object... args) {
+        return executeCommand(String.format(commandFormat, args));
     }
 
     private List<String> executeCommand(String command) {
@@ -256,15 +530,32 @@
         }
     }
 
+    private String readLine() throws IOException {
+        final CompletableFuture<String> future = new CompletableFuture<>();
+        mReaderHandler.post(
+                () -> {
+                    try {
+                        future.complete(mReader.readLine());
+                    } catch (IOException e) {
+                        future.completeExceptionally(e);
+                    }
+                });
+        try {
+            return future.get(READ_LINE_TIMEOUT_SECONDS, SECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            throw new IOException("Failed to read a line from ot-cli-ftd");
+        }
+    }
+
     private List<String> readUntilDone() throws IOException {
         ArrayList<String> result = new ArrayList<>();
         String line;
-        while ((line = mReader.readLine()) != null) {
+        while ((line = readLine()) != null) {
             if (line.equals("Done")) {
                 break;
             }
-            if (line.startsWith("Error:")) {
-                fail("ot-cli-ftd reported an error: " + line);
+            if (line.startsWith("Error")) {
+                throw new IOException("ot-cli-ftd reported an error: " + line);
             }
             if (!line.startsWith("> ")) {
                 result.add(line);
@@ -272,4 +563,68 @@
         }
         return result;
     }
+
+    private static String txtMapToHexString(Map<String, byte[]> txtMap) {
+        if (txtMap == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, byte[]> entry : txtMap.entrySet()) {
+            int length = entry.getKey().length() + entry.getValue().length + 1;
+            sb.append(String.format("%02x", length));
+            sb.append(toHexString(entry.getKey()));
+            sb.append(toHexString("="));
+            sb.append(toHexString(entry.getValue()));
+        }
+        return sb.toString();
+    }
+
+    private static String toHexString(String s) {
+        return toHexString(s.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static String toHexString(byte[] bytes) {
+        return base16().encode(bytes);
+    }
+
+    private static final class DnsServiceCliOutputParser {
+        /** Returns the first match in the input of a given regex pattern. */
+        private static Matcher firstMatchOf(String input, String regex) {
+            Matcher matcher = Pattern.compile(regex).matcher(input);
+            matcher.find();
+            return matcher;
+        }
+
+        // Example: "Port:12345"
+        private static int parsePort(String line) {
+            return Integer.parseInt(firstMatchOf(line, "Port:(\\d+)").group(1));
+        }
+
+        // Example: "Host:Android.default.service.arpa."
+        private static String parseHostname(String line) {
+            return firstMatchOf(line, "Host:(.+)").group(1);
+        }
+
+        // Example: "HostAddress:2001:0:0:0:0:0:0:1"
+        private static InetAddress parseHostAddress(String line) {
+            return InetAddresses.parseNumericAddress(
+                    firstMatchOf(line, "HostAddress:([^ ]+)").group(1));
+        }
+
+        // Example: "TXT:[key1=0102, key2=03]"
+        private static void parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo) {
+            String txtString = firstMatchOf(line, "TXT:\\[([^\\]]+)\\]").group(1);
+            for (String txtEntry : txtString.split(",")) {
+                String[] nameAndValue = txtEntry.trim().split("=");
+                String name = nameAndValue[0];
+                String value = nameAndValue[1];
+                byte[] bytes = new byte[value.length() / 2];
+                for (int i = 0; i < value.length(); i += 2) {
+                    byte b = (byte) ((value.charAt(i) - '0') << 4 | (value.charAt(i + 1) - '0'));
+                    bytes[i / 2] = b;
+                }
+                serviceInfo.setAttribute(name, bytes);
+            }
+        }
+    }
 }
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index 74251a6..82e9332 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -15,22 +15,44 @@
  */
 package android.net.thread.utils;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.system.OsConstants.IPPROTO_ICMP;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 
+import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
+import static org.junit.Assert.assertNotNull;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.net.ConnectivityManager;
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
 import android.net.TestNetworkInterface;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.ThreadNetworkController;
+import android.os.Build;
 import android.os.Handler;
 import android.os.SystemClock;
-import android.os.SystemProperties;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
 
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.Icmpv4Header;
 import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.PrefixInformationOption;
 import com.android.net.module.util.structs.RaHeader;
@@ -43,6 +65,7 @@
 import java.io.IOException;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
+import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -51,6 +74,7 @@
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -66,15 +90,21 @@
     public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(30);
     public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
     public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
+    public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
+
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+    private static final byte[] DEFAULT_DATASET_TLVS =
+            base16().decode(
+                            "0E080000000000010000000300001335060004001FFFE002"
+                                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                                    + "B9D351B40C0402A0FFF8");
+    public static final ActiveOperationalDataset DEFAULT_DATASET =
+            ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
 
     private IntegrationTestUtils() {}
 
-    /** Returns whether the device supports simulated Thread radio. */
-    public static boolean isSimulatedThreadRadioSupported() {
-        // The integration test uses SIMULATION Thread radio so that it only supports CuttleFish.
-        return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
-    }
-
     /**
      * Waits for the given {@link Supplier} to be true until given timeout.
      *
@@ -84,7 +114,7 @@
      */
     public static void waitFor(Supplier<Boolean> condition, Duration timeout)
             throws TimeoutException {
-        final long intervalMills = 1000;
+        final long intervalMills = 500;
         final long timeoutMills = timeout.toMillis();
 
         for (long i = 0; i < timeoutMills; i += intervalMills) {
@@ -166,16 +196,36 @@
         return null;
     }
 
-    /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
-    public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
-        if (packet == null) {
+    /** Returns {@code true} if {@code packet} is an ICMPv4 packet of given {@code type}. */
+    public static boolean isExpectedIcmpv4Packet(byte[] packet, int type) {
+        ByteBuffer buf = makeByteBuffer(packet);
+        Ipv4Header header = extractIpv4Header(buf);
+        if (header == null) {
             return false;
         }
-        ByteBuffer buf = ByteBuffer.wrap(packet);
+        if (header.protocol != (byte) IPPROTO_ICMP) {
+            return false;
+        }
         try {
-            if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
-                return false;
-            }
+            return Struct.parse(Icmpv4Header.class, buf).type == (short) type;
+        } catch (IllegalArgumentException ignored) {
+            // It's fine that the passed in packet is malformed because it's could be sent
+            // by anybody.
+        }
+        return false;
+    }
+
+    /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
+    public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
+        ByteBuffer buf = makeByteBuffer(packet);
+        Ipv6Header header = extractIpv6Header(buf);
+        if (header == null) {
+            return false;
+        }
+        if (header.nextHeader != (byte) IPPROTO_ICMPV6) {
+            return false;
+        }
+        try {
             return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
         } catch (IllegalArgumentException ignored) {
             // It's fine that the passed in packet is malformed because it's could be sent
@@ -184,32 +234,66 @@
         return false;
     }
 
-    public static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
-        if (packet == null) {
-            return false;
-        }
-        ByteBuffer buf = ByteBuffer.wrap(packet);
-        try {
-            return Struct.parse(Ipv6Header.class, buf).srcIp.equals(src);
-        } catch (IllegalArgumentException ignored) {
-            // It's fine that the passed in packet is malformed because it's could be sent
-            // by anybody.
+    public static boolean isFrom(byte[] packet, InetAddress src) {
+        if (src instanceof Inet4Address) {
+            return isFromIpv4Source(packet, (Inet4Address) src);
+        } else if (src instanceof Inet6Address) {
+            return isFromIpv6Source(packet, (Inet6Address) src);
         }
         return false;
     }
 
-    public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
-        if (packet == null) {
-            return false;
+    public static boolean isTo(byte[] packet, InetAddress dest) {
+        if (dest instanceof Inet4Address) {
+            return isToIpv4Destination(packet, (Inet4Address) dest);
+        } else if (dest instanceof Inet6Address) {
+            return isToIpv6Destination(packet, (Inet6Address) dest);
         }
-        ByteBuffer buf = ByteBuffer.wrap(packet);
+        return false;
+    }
+
+    private static boolean isFromIpv4Source(byte[] packet, Inet4Address src) {
+        Ipv4Header header = extractIpv4Header(makeByteBuffer(packet));
+        return header != null && header.srcIp.equals(src);
+    }
+
+    private static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
+        Ipv6Header header = extractIpv6Header(makeByteBuffer(packet));
+        return header != null && header.srcIp.equals(src);
+    }
+
+    private static boolean isToIpv4Destination(byte[] packet, Inet4Address dest) {
+        Ipv4Header header = extractIpv4Header(makeByteBuffer(packet));
+        return header != null && header.dstIp.equals(dest);
+    }
+
+    private static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
+        Ipv6Header header = extractIpv6Header(makeByteBuffer(packet));
+        return header != null && header.dstIp.equals(dest);
+    }
+
+    private static ByteBuffer makeByteBuffer(byte[] packet) {
+        return packet == null ? null : ByteBuffer.wrap(packet);
+    }
+
+    private static Ipv4Header extractIpv4Header(ByteBuffer buf) {
         try {
-            return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
+            return Struct.parse(Ipv4Header.class, buf);
         } catch (IllegalArgumentException ignored) {
             // It's fine that the passed in packet is malformed because it's could be sent
             // by anybody.
         }
-        return false;
+        return null;
+    }
+
+    private static Ipv6Header extractIpv6Header(ByteBuffer buf) {
+        try {
+            return Struct.parse(Ipv6Header.class, buf);
+        } catch (IllegalArgumentException ignored) {
+            // It's fine that the passed in packet is malformed because it's could be sent
+            // by anybody.
+        }
+        return null;
     }
 
     /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
@@ -289,4 +373,191 @@
         }
         return false;
     }
+
+    public static List<LinkAddress> getIpv6LinkAddresses(String interfaceName) {
+        List<LinkAddress> addresses = new ArrayList<>();
+        final String cmd = " ip -6 addr show dev " + interfaceName;
+        final String output = runShellCommandOrThrow(cmd);
+
+        for (final String line : output.split("\\n")) {
+            if (line.contains("inet6")) {
+                addresses.add(parseAddressLine(line));
+            }
+        }
+
+        return addresses;
+    }
+
+    /** Return the first discovered service of {@code serviceType}. */
+    public static NsdServiceInfo discoverService(NsdManager nsdManager, String serviceType)
+            throws Exception {
+        CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
+        NsdManager.DiscoveryListener listener =
+                new DefaultDiscoveryListener() {
+                    @Override
+                    public void onServiceFound(NsdServiceInfo serviceInfo) {
+                        serviceInfoFuture.complete(serviceInfo);
+                    }
+                };
+        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+        try {
+            serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+        } finally {
+            nsdManager.stopServiceDiscovery(listener);
+        }
+
+        return serviceInfoFuture.get();
+    }
+
+    /**
+     * Returns the {@link NsdServiceInfo} when a service instance of {@code serviceType} gets lost.
+     */
+    public static NsdManager.DiscoveryListener discoverForServiceLost(
+            NsdManager nsdManager,
+            String serviceType,
+            CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
+        NsdManager.DiscoveryListener listener =
+                new DefaultDiscoveryListener() {
+                    @Override
+                    public void onServiceLost(NsdServiceInfo serviceInfo) {
+                        serviceInfoFuture.complete(serviceInfo);
+                    }
+                };
+        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+        return listener;
+    }
+
+    /** Resolves the service. */
+    public static NsdServiceInfo resolveService(NsdManager nsdManager, NsdServiceInfo serviceInfo)
+            throws Exception {
+        return resolveServiceUntil(nsdManager, serviceInfo, s -> true);
+    }
+
+    /** Returns the first resolved service that satisfies the {@code predicate}. */
+    public static NsdServiceInfo resolveServiceUntil(
+            NsdManager nsdManager, NsdServiceInfo serviceInfo, Predicate<NsdServiceInfo> predicate)
+            throws Exception {
+        CompletableFuture<NsdServiceInfo> resolvedServiceInfoFuture = new CompletableFuture<>();
+        NsdManager.ServiceInfoCallback callback =
+                new DefaultServiceInfoCallback() {
+                    @Override
+                    public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+                        if (predicate.test(serviceInfo)) {
+                            resolvedServiceInfoFuture.complete(serviceInfo);
+                        }
+                    }
+                };
+        nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback);
+        try {
+            return resolvedServiceInfoFuture.get(
+                    SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+        } finally {
+            nsdManager.unregisterServiceInfoCallback(callback);
+        }
+    }
+
+    public static String getPrefixesFromNetData(String netData) {
+        int startIdx = netData.indexOf("Prefixes:");
+        int endIdx = netData.indexOf("Routes:");
+        return netData.substring(startIdx, endIdx);
+    }
+
+    public static Network getThreadNetwork(Duration timeout) throws Exception {
+        CompletableFuture<Network> networkFuture = new CompletableFuture<>();
+        ConnectivityManager cm =
+                ApplicationProvider.getApplicationContext()
+                        .getSystemService(ConnectivityManager.class);
+        NetworkRequest.Builder networkRequestBuilder =
+                new NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_THREAD);
+        // Before V, we need to explicitly set `NET_CAPABILITY_LOCAL_NETWORK` capability to request
+        // a Thread network.
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            networkRequestBuilder.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        }
+        NetworkRequest networkRequest = networkRequestBuilder.build();
+        ConnectivityManager.NetworkCallback networkCallback =
+                new ConnectivityManager.NetworkCallback() {
+                    @Override
+                    public void onAvailable(Network network) {
+                        networkFuture.complete(network);
+                    }
+                };
+        cm.registerNetworkCallback(networkRequest, networkCallback);
+        return networkFuture.get(timeout.toSeconds(), SECONDS);
+    }
+
+    /**
+     * Let the FTD join the specified Thread network and wait for border routing to be available.
+     *
+     * @return the OMR address
+     */
+    public static Inet6Address joinNetworkAndWaitForOmr(
+            FullThreadDevice ftd, ActiveOperationalDataset dataset) throws Exception {
+        ftd.factoryReset();
+        ftd.joinNetwork(dataset);
+        ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+        waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
+        Inet6Address ftdOmr = ftd.getOmrAddress();
+        assertNotNull(ftdOmr);
+        return ftdOmr;
+    }
+
+    private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
+        @Override
+        public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
+
+        @Override
+        public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
+
+        @Override
+        public void onDiscoveryStarted(String serviceType) {}
+
+        @Override
+        public void onDiscoveryStopped(String serviceType) {}
+
+        @Override
+        public void onServiceFound(NsdServiceInfo serviceInfo) {}
+
+        @Override
+        public void onServiceLost(NsdServiceInfo serviceInfo) {}
+    }
+
+    private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
+        @Override
+        public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
+
+        @Override
+        public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
+
+        @Override
+        public void onServiceLost() {}
+
+        @Override
+        public void onServiceInfoCallbackUnregistered() {}
+    }
+
+    /**
+     * Parses a line of output from "ip -6 addr show" into a {@link LinkAddress}.
+     *
+     * <p>Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
+     */
+    private static LinkAddress parseAddressLine(String line) {
+        String[] parts = line.trim().split("\\s+");
+        String addressString = parts[1];
+        String[] pieces = addressString.split("/", 2);
+        int prefixLength = Integer.parseInt(pieces[1]);
+        final InetAddress address = InetAddresses.parseNumericAddress(pieces[0]);
+        long deprecationTimeMillis =
+                line.contains("deprecated")
+                        ? SystemClock.elapsedRealtime()
+                        : LinkAddress.LIFETIME_PERMANENT;
+
+        return new LinkAddress(
+                address,
+                prefixLength,
+                0 /* flags */,
+                0 /* scope */,
+                deprecationTimeMillis,
+                LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
+    }
 }
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index 4a06fe8..15a3f5c 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -16,13 +16,16 @@
 
 package android.net.thread.utils;
 
+import android.annotation.Nullable;
 import android.net.InetAddresses;
+import android.net.IpPrefix;
 import android.os.SystemClock;
 
 import com.android.compatibility.common.util.SystemUtil;
 
 import java.net.Inet6Address;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -53,16 +56,91 @@
 
     /** Returns the list of IPv6 addresses on ot-daemon. */
     public List<Inet6Address> getAddresses() {
-        String output = executeCommand("ipaddr");
-        return Arrays.asList(output.split("\n")).stream()
-                .map(String::trim)
-                .filter(str -> !str.equals("Done"))
+        return executeCommandAndParse("ipaddr").stream()
                 .map(addr -> InetAddresses.parseNumericAddress(addr))
                 .map(inetAddr -> (Inet6Address) inetAddr)
                 .toList();
     }
 
+    /** Returns {@code true} if the Thread interface is up. */
+    public boolean isInterfaceUp() {
+        String output = executeCommand("ifconfig");
+        return output.contains("up");
+    }
+
+    /** Returns the ML-EID of the device. */
+    public Inet6Address getMlEid() {
+        String addressStr = executeCommandAndParse("ipaddr mleid").get(0);
+        return (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
+    }
+
+    /** Returns the country code on ot-daemon. */
+    public String getCountryCode() {
+        return executeCommandAndParse("region").get(0);
+    }
+
+    /**
+     * Returns the list of IPv6 Mesh-Local addresses on ot-daemon.
+     *
+     * <p>The return List can be empty if no Mesh-Local prefix exists.
+     */
+    public List<Inet6Address> getMeshLocalAddresses() {
+        IpPrefix meshLocalPrefix = getMeshLocalPrefix();
+        if (meshLocalPrefix == null) {
+            return Collections.emptyList();
+        }
+        return getAddresses().stream().filter(addr -> meshLocalPrefix.contains(addr)).toList();
+    }
+
+    /**
+     * Returns the Mesh-Local prefix or {@code null} if none exists (e.g. the Active Dataset is not
+     * set).
+     */
+    @Nullable
+    public IpPrefix getMeshLocalPrefix() {
+        List<IpPrefix> prefixes =
+                executeCommandAndParse("prefix meshlocal").stream()
+                        .map(prefix -> new IpPrefix(prefix))
+                        .toList();
+        return prefixes.isEmpty() ? null : prefixes.get(0);
+    }
+
+    /** Enables/Disables NAT64 feature. */
+    public void setNat64Enabled(boolean enabled) {
+        executeCommand("nat64 " + (enabled ? "enable" : "disable"));
+    }
+
+    /** Sets the NAT64 CIDR. */
+    public void setNat64Cidr(String cidr) {
+        executeCommand("nat64 cidr " + cidr);
+    }
+
+    /** Returns whether there's a NAT64 prefix in network data */
+    public boolean hasNat64PrefixInNetdata() {
+        // Example (in the 'Routes' section):
+        // fdb2:bae3:5b59:2:0:0::/96 sn low c000
+        List<String> outputLines = executeCommandAndParse("netdata show");
+        for (String line : outputLines) {
+            if (line.contains(" sn")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public String executeCommand(String cmd) {
         return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
     }
+
+    /**
+     * Executes a ot-ctl command and parse the output to a list of strings.
+     *
+     * <p>The trailing "Done" in the command output will be dropped.
+     */
+    public List<String> executeCommandAndParse(String cmd) {
+        return Arrays.asList(executeCommand(cmd).split("\n")).stream()
+                .map(String::trim)
+                .filter(str -> !str.equals("Done"))
+                .toList();
+    }
 }
diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
new file mode 100644
index 0000000..7e84233
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.thread.ThreadNetworkException;
+import android.net.thread.ThreadNetworkManager;
+import android.os.OutcomeReceiver;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/** A helper class which provides synchronous API wrappers for {@link ThreadNetworkController}. */
+public final class ThreadNetworkControllerWrapper {
+    public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(10);
+    public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
+    private static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
+    private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
+
+    private final ThreadNetworkController mController;
+
+    /**
+     * Returns a new {@link ThreadNetworkControllerWrapper} instance or {@code null} if Thread
+     * feature is not supported on this device.
+     */
+    @Nullable
+    public static ThreadNetworkControllerWrapper newInstance(Context context) {
+        final ThreadNetworkManager manager = context.getSystemService(ThreadNetworkManager.class);
+        if (manager == null) {
+            return null;
+        }
+        return new ThreadNetworkControllerWrapper(manager.getAllThreadNetworkControllers().get(0));
+    }
+
+    private ThreadNetworkControllerWrapper(ThreadNetworkController controller) {
+        mController = controller;
+    }
+
+    /**
+     * Returns the Thread enabled state.
+     *
+     * <p>The value can be one of {@code ThreadNetworkController#STATE_*}.
+     */
+    public final int getEnabledState()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback =
+                new StateCallback() {
+                    @Override
+                    public void onThreadEnableStateChanged(int enabledState) {
+                        future.complete(enabledState);
+                    }
+
+                    @Override
+                    public void onDeviceRoleChanged(int deviceRole) {}
+                };
+
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                () -> mController.registerStateCallback(directExecutor(), callback));
+        try {
+            return future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+        }
+    }
+
+    /**
+     * Returns the Thread device role.
+     *
+     * <p>The value can be one of {@code ThreadNetworkController#DEVICE_ROLE_*}.
+     */
+    public final int getDeviceRole()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        StateCallback callback = future::complete;
+
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                () -> mController.registerStateCallback(directExecutor(), callback));
+        try {
+            return future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+        }
+    }
+
+    /** An synchronous variant of {@link ThreadNetworkController#setEnabled}. */
+    public void setEnabledAndWait(boolean enabled)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        mController.setEnabled(
+                                enabled, directExecutor(), newOutcomeReceiver(future)));
+        future.get(SET_ENABLED_TIMEOUT.toSeconds(), SECONDS);
+    }
+
+    /** Joins the given network and wait for this device to become attached. */
+    public void joinAndWait(ActiveOperationalDataset activeDataset)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        mController.join(
+                                activeDataset, directExecutor(), newOutcomeReceiver(future)));
+        future.get(JOIN_TIMEOUT.toSeconds(), SECONDS);
+    }
+
+    /** An synchronous variant of {@link ThreadNetworkController#leave}. */
+    public void leaveAndWait() throws InterruptedException, ExecutionException, TimeoutException {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> mController.leave(directExecutor(), future::complete));
+        future.get(LEAVE_TIMEOUT.toSeconds(), SECONDS);
+    }
+
+    /** Waits for the device role to become {@code deviceRole}. */
+    public int waitForRole(int deviceRole, Duration timeout)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        return waitForRoleAnyOf(List.of(deviceRole), timeout);
+    }
+
+    /** Waits for the device role to become one of the values specified in {@code deviceRoles}. */
+    public int waitForRoleAnyOf(List<Integer> deviceRoles, Duration timeout)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        CompletableFuture<Integer> future = new CompletableFuture<>();
+        ThreadNetworkController.StateCallback callback =
+                newRole -> {
+                    if (deviceRoles.contains(newRole)) {
+                        future.complete(newRole);
+                    }
+                };
+
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                () -> mController.registerStateCallback(directExecutor(), callback));
+
+        try {
+            return future.get(timeout.toSeconds(), SECONDS);
+        } finally {
+            mController.unregisterStateCallback(callback);
+        }
+    }
+
+    /** An synchronous variant of {@link ThreadNetworkController#setTestNetworkAsUpstream}. */
+    public void setTestNetworkAsUpstreamAndWait(@Nullable String networkInterfaceName)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                NETWORK_SETTINGS,
+                () -> {
+                    mController.setTestNetworkAsUpstream(
+                            networkInterfaceName, directExecutor(), future::complete);
+                });
+        future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+    }
+
+    private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+            CompletableFuture<V> future) {
+        return new OutcomeReceiver<V, ThreadNetworkException>() {
+            @Override
+            public void onResult(V result) {
+                future.complete(result);
+            }
+
+            @Override
+            public void onError(ThreadNetworkException e) {
+                future.completeExceptionally(e);
+            }
+        };
+    }
+}
diff --git a/thread/tests/multidevices/Android.bp b/thread/tests/multidevices/Android.bp
new file mode 100644
index 0000000..050caa8
--- /dev/null
+++ b/thread/tests/multidevices/Android.bp
@@ -0,0 +1,43 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_team: "trendy_team_fwk_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+python_test_host {
+    name: "ThreadNetworkMultiDeviceTests",
+    main: "thread_network_multi_device_test.py",
+    srcs: ["thread_network_multi_device_test.py"],
+    test_config: "AndroidTest.xml",
+    libs: [
+        "mobly",
+    ],
+    test_options: {
+        unit_test: false,
+        tags: ["mobly"],
+    },
+    test_suites: [
+        "mts-tethering",
+        "general-tests",
+    ],
+    version: {
+        py3: {
+            embedded_launcher: true,
+        },
+    },
+}
diff --git a/thread/tests/multidevices/AndroidTest.xml b/thread/tests/multidevices/AndroidTest.xml
new file mode 100644
index 0000000..8b2bed3
--- /dev/null
+++ b/thread/tests/multidevices/AndroidTest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+ -->
+
+<configuration description="Config for Thread Multi-device test cases">
+    <option name="config-descriptor:metadata" key="component" value="threadnetwork" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
+
+    <object class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController"
+        type="module_controller">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+    </object>
+    <!--
+        Only run tests if the device under test is SDK version 34 (Android 14) or above.
+    -->
+    <object type="module_controller"
+        class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+    <device name="device1">
+        <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    </device>
+    <device name="device2">
+        <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    </device>
+
+    <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
+      <!-- The mobly-par-file-name should match the module name -->
+      <option name="mobly-par-file-name" value="ThreadNetworkMultiDeviceTests" />
+      <!-- Timeout limit in milliseconds for all test cases of the python binary -->
+      <option name="mobly-test-timeout" value="180000" />
+    </test>
+</configuration>
diff --git a/thread/tests/multidevices/thread_network_multi_device_test.py b/thread/tests/multidevices/thread_network_multi_device_test.py
new file mode 100644
index 0000000..652576b
--- /dev/null
+++ b/thread/tests/multidevices/thread_network_multi_device_test.py
@@ -0,0 +1,67 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+# Lint as: python3
+
+import logging
+import time
+
+from mobly import asserts
+from mobly import base_test
+from mobly import test_runner
+from mobly.controllers import android_device
+
+class ThreadNetworkMultiDeviceTest(base_test.BaseTestClass):
+    def setup_class(self):
+        self.node_a, self.node_b = self.register_controller(
+            android_device, min_number=2)
+        self.node_a.adb.shell([
+            'ot-ctl', 'factoryreset',
+        ])
+        self.node_b.adb.shell([
+            'ot-ctl', 'factoryreset',
+        ])
+        time.sleep(1)
+
+    def ot_ctl(self, node, cmd, expect_done=True):
+        args = cmd.split(' ')
+        args = ['ot-ctl'] + args
+        stdout = node.adb.shell(args).decode('utf-8')
+        if expect_done:
+            asserts.assert_in('Done', stdout)
+        return stdout
+
+    def test_b_should_be_able_to_discover_a(self):
+        self.ot_ctl(self.node_a, 'dataset init new')
+        self.ot_ctl(self.node_a, 'dataset commit active')
+        self.ot_ctl(self.node_a, 'ifconfig up')
+        self.ot_ctl(self.node_a, 'thread start')
+        self.ot_ctl(self.node_a, 'state leader')
+        stdout = self.ot_ctl(self.node_a, 'extaddr')
+        extaddr = stdout.splitlines()[0]
+        logging.info('node a extaddr: %s', extaddr)
+        asserts.assert_equal(len(extaddr), 16)
+
+        stdout = self.ot_ctl(self.node_b, 'scan')
+        asserts.assert_in(extaddr, stdout)
+        logging.info('discovered node a')
+
+
+if __name__ == '__main__':
+    # Take test args
+    if '--' in sys.argv:
+        index = sys.argv.index('--')
+        sys.argv = sys.argv[:1] + sys.argv[index + 1:]
+
+    test_runner.main()
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 3365cd0..9404d1b 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -33,6 +33,7 @@
         "mts-tethering",
     ],
     static_libs: [
+        "androidx.test.rules",
         "frameworks-base-testutils",
         "framework-connectivity-pre-jarjar",
         "framework-connectivity-t-pre-jarjar",
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
index ace7c52..8442e80 100644
--- a/thread/tests/unit/AndroidManifest.xml
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -19,6 +19,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.net.thread.unittests">
 
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index d16e423..58e9bdd 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -31,6 +31,11 @@
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
 
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+        <option name="required-feature" value="android.hardware.thread_network" />
+    </object>
+
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
         <option name="check-min-sdk" value="true" />
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
index 2244a89..11c78e3 100644
--- a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -41,6 +41,19 @@
     }
 
     @Test
+    public void fromInstant_authoritativeIsSetAsSpecified() {
+        Instant instant = Instant.now();
+
+        OperationalDatasetTimestamp timestampAuthoritativeFalse =
+                OperationalDatasetTimestamp.fromInstant(instant, false);
+        OperationalDatasetTimestamp timestampAuthoritativeTrue =
+                OperationalDatasetTimestamp.fromInstant(instant, true);
+
+        assertThat(timestampAuthoritativeFalse.isAuthoritativeSource()).isEqualTo(false);
+        assertThat(timestampAuthoritativeTrue.isAuthoritativeSource()).isEqualTo(true);
+    }
+
+    @Test
     public void fromTlvValue_goodValue_success() {
         OperationalDatasetTimestamp timestamp =
                 OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989"));
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 75eb043..ac74372 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -19,6 +19,7 @@
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
 import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
 import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
 import static android.os.Process.SYSTEM_UID;
 
 import static com.google.common.io.BaseEncoding.base16;
@@ -33,6 +34,7 @@
 import android.os.Binder;
 import android.os.OutcomeReceiver;
 import android.os.Process;
+import android.util.SparseIntArray;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -77,6 +79,13 @@
     private static final ActiveOperationalDataset DEFAULT_DATASET =
             ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
 
+    private static final SparseIntArray DEFAULT_CHANNEL_POWERS =
+            new SparseIntArray() {
+                {
+                    put(20, 32767);
+                }
+            };
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -111,6 +120,10 @@
         return (IOperationReceiver) invocation.getArguments()[1];
     }
 
+    private static IOperationReceiver getSetChannelMaxPowersReceiver(InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[1];
+    }
+
     private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
             InvocationOnMock invocation) {
         return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
@@ -361,6 +374,51 @@
     }
 
     @Test
+    public void setChannelMaxPowers_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+        setBinderUid(SYSTEM_UID);
+
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+        doAnswer(
+                        invoke -> {
+                            getSetChannelMaxPowersReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .setChannelMaxPowers(any(ChannelMaxPower[].class), any(IOperationReceiver.class));
+        mController.setChannelMaxPowers(
+                DEFAULT_CHANNEL_POWERS,
+                Runnable::run,
+                v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getSetChannelMaxPowersReceiver(invoke)
+                                    .onError(ERROR_UNSUPPORTED_OPERATION, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .setChannelMaxPowers(any(ChannelMaxPower[].class), any(IOperationReceiver.class));
+        mController.setChannelMaxPowers(
+                DEFAULT_CHANNEL_POWERS,
+                Runnable::run,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(Void unused) {}
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        errorCallbackUid.set(Binder.getCallingUid());
+                    }
+                });
+
+        assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+        assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+    }
+
+    @Test
     public void setTestNetworkAsUpstream_callbackIsInvokedWithCallingAppIdentity()
             throws Exception {
         setBinderUid(SYSTEM_UID);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
index f62b437..5908c20 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
@@ -32,6 +32,6 @@
     public void constructor_tooLargeErrorCode_throwsIllegalArgumentException() throws Exception {
         // TODO (b/323791003): move this test case to cts/ThreadNetworkExceptionTest when mainline
         // CTS is ready.
-        assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(13, "13"));
+        assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(14, "14"));
     }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index 8aea0a3..b32986d 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -16,43 +16,73 @@
 
 package com.android.server.thread;
 
+import static android.net.DnsResolver.ERROR_SYSTEM;
 import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
 import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.hamcrest.MockitoHamcrest.argThat;
 
+import android.net.DnsResolver;
+import android.net.InetAddresses;
+import android.net.Network;
+import android.net.nsd.DiscoveryRequest;
 import android.net.nsd.NsdManager;
 import android.net.nsd.NsdServiceInfo;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.test.TestLooper;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
 import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
+import com.android.server.thread.openthread.INsdResolveHostCallback;
+import com.android.server.thread.openthread.INsdResolveServiceCallback;
 import com.android.server.thread.openthread.INsdStatusReceiver;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.net.InetAddress;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
 /** Unit tests for {@link NsdPublisher}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
 public final class NsdPublisherTest {
+    private static final DnsTxtAttribute TEST_TXT_ENTRY_1 =
+            new DnsTxtAttribute("key1", new byte[] {0x01, 0x02});
+    private static final DnsTxtAttribute TEST_TXT_ENTRY_2 =
+            new DnsTxtAttribute("key2", new byte[] {0x03});
+
     @Mock private NsdManager mMockNsdManager;
+    @Mock private DnsResolver mMockDnsResolver;
 
     @Mock private INsdStatusReceiver mRegistrationReceiver;
     @Mock private INsdStatusReceiver mUnregistrationReceiver;
+    @Mock private INsdDiscoverServiceCallback mDiscoverServiceCallback;
+    @Mock private INsdResolveServiceCallback mResolveServiceCallback;
+    @Mock private INsdResolveHostCallback mResolveHostCallback;
+    @Mock private Network mNetwork;
 
     private TestLooper mTestLooper;
     private NsdPublisher mNsdPublisher;
@@ -66,19 +96,15 @@
     public void registerService_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         mNsdPublisher.registerService(
                 null,
                 "MyService",
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
-
         mTestLooper.dispatchAll();
 
         ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
@@ -105,11 +131,10 @@
         assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
         assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
         assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
-        assertThat(actualServiceInfo.getAttributes().get("key1"))
-                .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
-        assertThat(actualServiceInfo.getAttributes().get("key2"))
-                .isEqualTo(new byte[] {(byte) 0x03});
-
+        assertThat(actualServiceInfo.getAttributes().get(TEST_TXT_ENTRY_1.name))
+                .isEqualTo(TEST_TXT_ENTRY_1.value);
+        assertThat(actualServiceInfo.getAttributes().get(TEST_TXT_ENTRY_2.name))
+                .isEqualTo(TEST_TXT_ENTRY_2.value);
         verify(mRegistrationReceiver, times(1)).onSuccess();
     }
 
@@ -117,19 +142,15 @@
     public void registerService_nsdManagerFails_serviceRegistrationFails() throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         mNsdPublisher.registerService(
                 null,
                 "MyService",
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
-
         mTestLooper.dispatchAll();
 
         ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
@@ -156,21 +177,16 @@
         assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
         assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
         assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
-        assertThat(actualServiceInfo.getAttributes().get("key1"))
-                .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
-        assertThat(actualServiceInfo.getAttributes().get("key2"))
-                .isEqualTo(new byte[] {(byte) 0x03});
-
+        assertThat(actualServiceInfo.getAttributes().get(TEST_TXT_ENTRY_1.name))
+                .isEqualTo(TEST_TXT_ENTRY_1.value);
+        assertThat(actualServiceInfo.getAttributes().get(TEST_TXT_ENTRY_2.name))
+                .isEqualTo(TEST_TXT_ENTRY_2.value);
         verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
     }
 
     @Test
     public void registerService_nsdManagerThrows_serviceRegistrationFails() throws Exception {
         prepareTest();
-
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         doThrow(new IllegalArgumentException("NsdManager fails"))
                 .when(mMockNsdManager)
                 .registerService(any(), anyInt(), any(Executor.class), any());
@@ -181,7 +197,7 @@
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
         mTestLooper.dispatchAll();
@@ -194,16 +210,13 @@
             throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         mNsdPublisher.registerService(
                 null,
                 "MyService",
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
 
@@ -239,16 +252,13 @@
     public void unregisterService_nsdManagerFails_serviceUnregistrationFails() throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
-
         mNsdPublisher.registerService(
                 null,
                 "MyService",
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
 
@@ -282,12 +292,425 @@
     }
 
     @Test
-    public void onOtDaemonDied_unregisterAll() {
+    public void registerHost_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
         prepareTest();
 
-        DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
-        DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
 
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(),
+                        actualRegistrationListenerCaptor.capture());
+
+        NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+        NsdManager.RegistrationListener actualRegistrationListener =
+                actualRegistrationListenerCaptor.getValue();
+
+        actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+        mTestLooper.dispatchAll();
+
+        assertThat(actualServiceInfo.getServiceName()).isNull();
+        assertThat(actualServiceInfo.getServiceType()).isNull();
+        assertThat(actualServiceInfo.getSubtypes()).isEmpty();
+        assertThat(actualServiceInfo.getPort()).isEqualTo(0);
+        assertThat(actualServiceInfo.getAttributes()).isEmpty();
+        assertThat(actualServiceInfo.getHostname()).isEqualTo("MyHost");
+        assertThat(actualServiceInfo.getHostAddresses())
+                .isEqualTo(makeAddresses("2001:db8::1", "2001:db8::2", "2001:db8::3"));
+
+        verify(mRegistrationReceiver, times(1)).onSuccess();
+    }
+
+    @Test
+    public void registerHost_nsdManagerFails_serviceRegistrationFails() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(),
+                        actualRegistrationListenerCaptor.capture());
+        mTestLooper.dispatchAll();
+
+        NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+        NsdManager.RegistrationListener actualRegistrationListener =
+                actualRegistrationListenerCaptor.getValue();
+
+        actualRegistrationListener.onRegistrationFailed(actualServiceInfo, FAILURE_INTERNAL_ERROR);
+        mTestLooper.dispatchAll();
+
+        assertThat(actualServiceInfo.getServiceName()).isNull();
+        assertThat(actualServiceInfo.getServiceType()).isNull();
+        assertThat(actualServiceInfo.getSubtypes()).isEmpty();
+        assertThat(actualServiceInfo.getPort()).isEqualTo(0);
+        assertThat(actualServiceInfo.getAttributes()).isEmpty();
+        assertThat(actualServiceInfo.getHostname()).isEqualTo("MyHost");
+        assertThat(actualServiceInfo.getHostAddresses())
+                .isEqualTo(makeAddresses("2001:db8::1", "2001:db8::2", "2001:db8::3"));
+
+        verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void registerHost_nsdManagerThrows_serviceRegistrationFails() throws Exception {
+        prepareTest();
+
+        doThrow(new IllegalArgumentException("NsdManager fails"))
+                .when(mMockNsdManager)
+                .registerService(any(), anyInt(), any(Executor.class), any());
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void unregisterHost_nsdManagerSucceeds_serviceUnregistrationSucceeds() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(Executor.class),
+                        actualRegistrationListenerCaptor.capture());
+
+        NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+        NsdManager.RegistrationListener actualRegistrationListener =
+                actualRegistrationListenerCaptor.getValue();
+
+        actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+        mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+        mTestLooper.dispatchAll();
+        verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+        actualRegistrationListener.onServiceUnregistered(actualServiceInfo);
+        mTestLooper.dispatchAll();
+        verify(mUnregistrationReceiver, times(1)).onSuccess();
+    }
+
+    @Test
+    public void unregisterHost_nsdManagerFails_serviceUnregistrationFails() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.registerHost(
+                "MyHost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                16 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+                ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+        verify(mMockNsdManager, times(1))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(Executor.class),
+                        actualRegistrationListenerCaptor.capture());
+
+        NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+        NsdManager.RegistrationListener actualRegistrationListener =
+                actualRegistrationListenerCaptor.getValue();
+
+        actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+        mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+        mTestLooper.dispatchAll();
+        verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+        actualRegistrationListener.onUnregistrationFailed(
+                actualServiceInfo, FAILURE_INTERNAL_ERROR);
+        mTestLooper.dispatchAll();
+        verify(mUnregistrationReceiver, times(1)).onError(0);
+    }
+
+    @Test
+    public void discoverService_serviceDiscovered() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+        ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+                ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+        verify(mMockNsdManager, times(1))
+                .discoverServices(
+                        eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+                        any(Executor.class),
+                        discoveryListenerArgumentCaptor.capture());
+        NsdManager.DiscoveryListener actualDiscoveryListener =
+                discoveryListenerArgumentCaptor.getValue();
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+        serviceInfo.setServiceName("test");
+        serviceInfo.setServiceType(null);
+        actualDiscoveryListener.onServiceFound(serviceInfo);
+        mTestLooper.dispatchAll();
+
+        verify(mDiscoverServiceCallback, times(1))
+                .onServiceDiscovered("test", "_test._tcp", true /* isFound */);
+    }
+
+    @Test
+    public void discoverService_serviceLost() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+        ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+                ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+        verify(mMockNsdManager, times(1))
+                .discoverServices(
+                        eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+                        any(Executor.class),
+                        discoveryListenerArgumentCaptor.capture());
+        NsdManager.DiscoveryListener actualDiscoveryListener =
+                discoveryListenerArgumentCaptor.getValue();
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+        serviceInfo.setServiceName("test");
+        serviceInfo.setServiceType(null);
+        actualDiscoveryListener.onServiceLost(serviceInfo);
+        mTestLooper.dispatchAll();
+
+        verify(mDiscoverServiceCallback, times(1))
+                .onServiceDiscovered("test", "_test._tcp", false /* isFound */);
+    }
+
+    @Test
+    public void stopServiceDiscovery() {
+        prepareTest();
+
+        mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+        ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+                ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+        verify(mMockNsdManager, times(1))
+                .discoverServices(
+                        eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+                        any(Executor.class),
+                        discoveryListenerArgumentCaptor.capture());
+        NsdManager.DiscoveryListener actualDiscoveryListener =
+                discoveryListenerArgumentCaptor.getValue();
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+        serviceInfo.setServiceName("test");
+        serviceInfo.setServiceType(null);
+        actualDiscoveryListener.onServiceFound(serviceInfo);
+        mNsdPublisher.stopServiceDiscovery(10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        verify(mMockNsdManager, times(1)).stopServiceDiscovery(actualDiscoveryListener);
+    }
+
+    @Test
+    public void resolveService_serviceResolved() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveService(
+                "test", "_test._tcp", mResolveServiceCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+        ArgumentCaptor<NsdServiceInfo> serviceInfoArgumentCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.ServiceInfoCallback> serviceInfoCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(NsdManager.ServiceInfoCallback.class);
+        verify(mMockNsdManager, times(1))
+                .registerServiceInfoCallback(
+                        serviceInfoArgumentCaptor.capture(),
+                        any(Executor.class),
+                        serviceInfoCallbackArgumentCaptor.capture());
+        assertThat(serviceInfoArgumentCaptor.getValue().getServiceName()).isEqualTo("test");
+        assertThat(serviceInfoArgumentCaptor.getValue().getServiceType()).isEqualTo("_test._tcp");
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+        serviceInfo.setServiceName("test");
+        serviceInfo.setServiceType("_test._tcp");
+        serviceInfo.setPort(12345);
+        serviceInfo.setHostname("test-host");
+        serviceInfo.setHostAddresses(
+                List.of(
+                        InetAddress.parseNumericAddress("2001::1"),
+                        InetAddress.parseNumericAddress("2001::2")));
+        serviceInfo.setAttribute(TEST_TXT_ENTRY_1.name, TEST_TXT_ENTRY_1.value);
+        serviceInfo.setAttribute(TEST_TXT_ENTRY_2.name, TEST_TXT_ENTRY_2.value);
+        serviceInfoCallbackArgumentCaptor.getValue().onServiceUpdated(serviceInfo);
+        mTestLooper.dispatchAll();
+
+        verify(mResolveServiceCallback, times(1))
+                .onServiceResolved(
+                        eq("test-host"),
+                        eq("test"),
+                        eq("_test._tcp"),
+                        eq(12345),
+                        eq(List.of("2001::1", "2001::2")),
+                        (List<DnsTxtAttribute>)
+                                argThat(containsInAnyOrder(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2)),
+                        anyInt());
+    }
+
+    @Test
+    public void stopServiceResolution() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveService(
+                "test", "_test._tcp", mResolveServiceCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+        ArgumentCaptor<NsdServiceInfo> serviceInfoArgumentCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        ArgumentCaptor<NsdManager.ServiceInfoCallback> serviceInfoCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(NsdManager.ServiceInfoCallback.class);
+        verify(mMockNsdManager, times(1))
+                .registerServiceInfoCallback(
+                        serviceInfoArgumentCaptor.capture(),
+                        any(Executor.class),
+                        serviceInfoCallbackArgumentCaptor.capture());
+        assertThat(serviceInfoArgumentCaptor.getValue().getServiceName()).isEqualTo("test");
+        assertThat(serviceInfoArgumentCaptor.getValue().getServiceType()).isEqualTo("_test._tcp");
+        NsdServiceInfo serviceInfo = new NsdServiceInfo();
+        serviceInfo.setServiceName("test");
+        serviceInfo.setServiceType("_test._tcp");
+        serviceInfo.setPort(12345);
+        serviceInfo.setHostname("test-host");
+        serviceInfo.setHostAddresses(
+                List.of(
+                        InetAddress.parseNumericAddress("2001::1"),
+                        InetAddress.parseNumericAddress("2001::2")));
+        serviceInfo.setAttribute("key1", new byte[] {(byte) 0x01, (byte) 0x02});
+        serviceInfo.setAttribute("key2", new byte[] {(byte) 0x03});
+        serviceInfoCallbackArgumentCaptor.getValue().onServiceUpdated(serviceInfo);
+        mNsdPublisher.stopServiceResolution(10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        verify(mMockNsdManager, times(1))
+                .unregisterServiceInfoCallback(serviceInfoCallbackArgumentCaptor.getValue());
+    }
+
+    @Test
+    public void resolveHost_hostResolved() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DnsResolver.Callback<List<InetAddress>>> resolveHostCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(DnsResolver.Callback.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        any(CancellationSignal.class),
+                        resolveHostCallbackArgumentCaptor.capture());
+        resolveHostCallbackArgumentCaptor
+                .getValue()
+                .onAnswer(
+                        List.of(
+                                InetAddresses.parseNumericAddress("2001::1"),
+                                InetAddresses.parseNumericAddress("2001::2")),
+                        0);
+        mTestLooper.dispatchAll();
+
+        verify(mResolveHostCallback, times(1))
+                .onHostResolved("test", List.of("2001::1", "2001::2"));
+    }
+
+    @Test
+    public void resolveHost_errorReported() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DnsResolver.Callback<List<InetAddress>>> resolveHostCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(DnsResolver.Callback.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        any(CancellationSignal.class),
+                        resolveHostCallbackArgumentCaptor.capture());
+        resolveHostCallbackArgumentCaptor
+                .getValue()
+                .onError(new DnsResolver.DnsException(ERROR_SYSTEM, null /* cause */));
+        mTestLooper.dispatchAll();
+
+        verify(mResolveHostCallback, times(1)).onHostResolved("test", Collections.emptyList());
+    }
+
+    @Test
+    public void stopHostResolution() throws Exception {
+        prepareTest();
+
+        mNsdPublisher.resolveHost("test", mResolveHostCallback, 10 /* listenerId */);
+        mTestLooper.dispatchAll();
+        ArgumentCaptor<CancellationSignal> cancellationSignalArgumentCaptor =
+                ArgumentCaptor.forClass(CancellationSignal.class);
+        verify(mMockDnsResolver, times(1))
+                .query(
+                        eq(mNetwork),
+                        eq("test.local"),
+                        eq(DnsResolver.FLAG_NO_CACHE_LOOKUP),
+                        any(Executor.class),
+                        cancellationSignalArgumentCaptor.capture(),
+                        any(DnsResolver.Callback.class));
+
+        mNsdPublisher.stopHostResolution(10 /* listenerId */);
+        mTestLooper.dispatchAll();
+
+        assertThat(cancellationSignalArgumentCaptor.getValue().isCanceled()).isTrue();
+    }
+
+    @Test
+    public void reset_unregisterAll() {
+        prepareTest();
         ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
                 ArgumentCaptor.forClass(NsdServiceInfo.class);
         ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
@@ -299,7 +722,7 @@
                 "_test._tcp",
                 List.of("_subtype1", "_subtype2"),
                 12345,
-                List.of(txt1, txt2),
+                List.of(TEST_TXT_ENTRY_1, TEST_TXT_ENTRY_2),
                 mRegistrationReceiver,
                 16 /* listenerId */);
         mTestLooper.dispatchAll();
@@ -336,24 +759,50 @@
                 actualRegistrationListenerCaptor.getAllValues().get(1);
         actualListener2.onServiceRegistered(actualServiceInfoCaptor.getValue());
 
-        mNsdPublisher.onOtDaemonDied();
+        mNsdPublisher.registerHost(
+                "Myhost",
+                List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+                mRegistrationReceiver,
+                18 /* listenerId */);
+
+        mTestLooper.dispatchAll();
+
+        verify(mMockNsdManager, times(3))
+                .registerService(
+                        actualServiceInfoCaptor.capture(),
+                        eq(PROTOCOL_DNS_SD),
+                        any(Executor.class),
+                        actualRegistrationListenerCaptor.capture());
+        NsdManager.RegistrationListener actualListener3 =
+                actualRegistrationListenerCaptor.getAllValues().get(1);
+        actualListener3.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
+        mNsdPublisher.reset();
         mTestLooper.dispatchAll();
 
         verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
         verify(mMockNsdManager, times(1)).unregisterService(actualListener2);
+        verify(mMockNsdManager, times(1)).unregisterService(actualListener3);
     }
 
-    private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
-        DnsTxtAttribute txtAttribute = new DnsTxtAttribute();
+    @Test
+    public void onOtDaemonDied_resetIsCalled() {
+        prepareTest();
+        NsdPublisher spyNsdPublisher = spy(mNsdPublisher);
 
-        txtAttribute.name = name;
-        txtAttribute.value = new byte[value.size()];
+        spyNsdPublisher.onOtDaemonDied();
+        mTestLooper.dispatchAll();
 
-        for (int i = 0; i < value.size(); ++i) {
-            txtAttribute.value[i] = value.get(i).byteValue();
+        verify(spyNsdPublisher, times(1)).reset();
+    }
+
+    private static List<InetAddress> makeAddresses(String... addressStrings) {
+        List<InetAddress> addresses = new ArrayList<>();
+
+        for (String addressString : addressStrings) {
+            addresses.add(InetAddresses.parseNumericAddress(addressString));
         }
-
-        return txtAttribute;
+        return addresses;
     }
 
     // @Before and @Test run in different threads. NsdPublisher requires the jobs are run on the
@@ -362,6 +811,7 @@
     private void prepareTest() {
         mTestLooper = new TestLooper();
         Handler handler = new Handler(mTestLooper.getLooper());
-        mNsdPublisher = new NsdPublisher(mMockNsdManager, handler);
+        mNsdPublisher = new NsdPublisher(mMockNsdManager, mMockDnsResolver, handler);
+        mNsdPublisher.setNetworkForHostResolution(mNetwork);
     }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 60a5f2b..be32764 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,14 +16,22 @@
 
 package com.android.server.thread;
 
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_THREAD;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
 import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
 import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
 import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
 import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
 import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
+import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
 
 import static com.google.common.io.BaseEncoding.base16;
@@ -33,10 +41,14 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -44,37 +56,60 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.NetworkAgent;
 import android.net.NetworkProvider;
+import android.net.NetworkRequest;
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.IActiveOperationalDatasetReceiver;
 import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadConfiguration;
 import android.net.thread.ThreadNetworkException;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.UserManager;
 import android.os.test.TestLooper;
+import android.util.AtomicFile;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.connectivity.resources.R;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.MeshcopTxtAttributes;
 import com.android.server.thread.openthread.testing.FakeOtDaemon;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
 
+import java.time.Clock;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicReference;
@@ -82,6 +117,12 @@
 /** Unit tests for {@link ThreadNetworkControllerService}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+// This test doesn't really need to run on the UI thread, but @Before and @Test annotated methods
+// need to run in the same thread because there are code in {@code ThreadNetworkControllerService}
+// checking that all its methods are running in the thread of the handler it's using. This is due
+// to a bug in TestLooper that it executes all tasks on the current thread rather than the thread
+// associated to the backed Looper object.
+@UiThreadTest
 public final class ThreadNetworkControllerServiceTest {
     // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
     // Active Timestamp: 1
@@ -106,27 +147,42 @@
     private static final String DEFAULT_NETWORK_NAME = "thread-wpan0";
     private static final int OT_ERROR_NONE = 0;
     private static final int DEFAULT_SUPPORTED_CHANNEL_MASK = 0x07FFF800; // from channel 11 to 26
-    private static final int DEFAULT_PREFERRED_CHANNEL_MASK = 0x00000800; // channel 11
+
+    // The DEFAULT_PREFERRED_CHANNEL_MASK is the ot-daemon preferred channel mask. Channel 25 and
+    // 26 are not preferred by dataset. The ThreadNetworkControllerService will only select channel
+    // 11 when it creates randomized dataset.
+    private static final int DEFAULT_PREFERRED_CHANNEL_MASK = 0x06000800; // channel 11, 25 and 26
     private static final int DEFAULT_SELECTED_CHANNEL = 11;
     private static final byte[] DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY = base16().decode("001FFFE0");
 
+    private static final String TEST_VENDOR_OUI = "AC-DE-48";
+    private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
+    private static final String TEST_VENDOR_NAME = "test vendor";
+    private static final String TEST_MODEL_NAME = "test model";
+
     @Mock private ConnectivityManager mMockConnectivityManager;
     @Mock private NetworkAgent mMockNetworkAgent;
     @Mock private TunInterfaceController mMockTunIfController;
     @Mock private ParcelFileDescriptor mMockTunFd;
     @Mock private InfraInterfaceController mMockInfraIfController;
-    @Mock private ThreadPersistentSettings mMockPersistentSettings;
     @Mock private NsdPublisher mMockNsdPublisher;
     @Mock private UserManager mMockUserManager;
     @Mock private IBinder mIBinder;
+    @Mock Resources mResources;
+    @Mock ConnectivityResources mConnectivityResources;
+
     private Context mContext;
     private TestLooper mTestLooper;
     private FakeOtDaemon mFakeOtDaemon;
+    private ThreadPersistentSettings mPersistentSettings;
     private ThreadNetworkControllerService mService;
     @Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
 
+    @Rule(order = 1)
+    public final TemporaryFolder tempFolder = new TemporaryFolder();
+
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
         mContext = spy(ApplicationProvider.getApplicationContext());
@@ -134,6 +190,9 @@
                 .when(mContext)
                 .enforceCallingOrSelfPermission(
                         eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+        doNothing()
+                .when(mContext)
+                .enforceCallingOrSelfPermission(eq(NETWORK_SETTINGS), anyString());
 
         mTestLooper = new TestLooper();
         final Handler handler = new Handler(mTestLooper.getLooper());
@@ -143,9 +202,23 @@
         mFakeOtDaemon = new FakeOtDaemon(handler);
         when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
 
-        when(mMockPersistentSettings.get(any())).thenReturn(true);
         when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
 
+        when(mConnectivityResources.get()).thenReturn(mResources);
+        when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+        when(mResources.getString(eq(R.string.config_thread_vendor_name)))
+                .thenReturn(TEST_VENDOR_NAME);
+        when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
+                .thenReturn(TEST_VENDOR_OUI);
+        when(mResources.getString(eq(R.string.config_thread_model_name)))
+                .thenReturn(TEST_MODEL_NAME);
+        when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
+                .thenReturn(new String[] {});
+
+        final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
+        mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
+        mPersistentSettings.initialize();
+
         mService =
                 new ThreadNetworkControllerService(
                         mContext,
@@ -155,9 +228,11 @@
                         mMockConnectivityManager,
                         mMockTunIfController,
                         mMockInfraIfController,
-                        mMockPersistentSettings,
+                        mPersistentSettings,
                         mMockNsdPublisher,
-                        mMockUserManager);
+                        mMockUserManager,
+                        mConnectivityResources,
+                        () -> DEFAULT_COUNTRY_CODE);
         mService.setTestNetworkAgent(mMockNetworkAgent);
     }
 
@@ -174,6 +249,152 @@
     }
 
     @Test
+    public void initialize_resourceOverlayValuesAreSetToOtDaemon() throws Exception {
+        when(mResources.getString(eq(R.string.config_thread_vendor_name)))
+                .thenReturn(TEST_VENDOR_NAME);
+        when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
+                .thenReturn(TEST_VENDOR_OUI);
+        when(mResources.getString(eq(R.string.config_thread_model_name)))
+                .thenReturn(TEST_MODEL_NAME);
+        when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
+                .thenReturn(new String[] {"vt=test"});
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        MeshcopTxtAttributes meshcopTxts = mFakeOtDaemon.getOverriddenMeshcopTxtAttributes();
+        assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
+        assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);
+        assertThat(meshcopTxts.modelName).isEqualTo(TEST_MODEL_NAME);
+        assertThat(meshcopTxts.nonStandardTxtEntries)
+                .containsExactly(new DnsTxtAttribute("vt", "test".getBytes(UTF_8)));
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_emptyVendorName_accepted() {
+        when(mResources.getString(eq(R.string.config_thread_vendor_name))).thenReturn("");
+
+        MeshcopTxtAttributes meshcopTxts =
+                ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
+
+        assertThat(meshcopTxts.vendorName).isEqualTo("");
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_tooLongVendorName_throwsIllegalStateException() {
+        when(mResources.getString(eq(R.string.config_thread_vendor_name)))
+                .thenReturn("vendor name is 25 bytes!!");
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources));
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_tooLongModelName_throwsIllegalStateException() {
+        when(mResources.getString(eq(R.string.config_thread_model_name)))
+                .thenReturn("model name is 25 bytes!!!");
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources));
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_emptyModelName_accepted() {
+        when(mResources.getString(eq(R.string.config_thread_model_name))).thenReturn("");
+
+        var meshcopTxts = ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
+        assertThat(meshcopTxts.modelName).isEqualTo("");
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_invalidVendorOui_throwsIllegalStateException() {
+        assertThrows(
+                IllegalStateException.class, () -> getMeshcopTxtAttributesWithVendorOui("ABCDEFA"));
+        assertThrows(
+                IllegalStateException.class, () -> getMeshcopTxtAttributesWithVendorOui("ABCDEG"));
+        assertThrows(
+                IllegalStateException.class, () -> getMeshcopTxtAttributesWithVendorOui("ABCD"));
+        assertThrows(
+                IllegalStateException.class,
+                () -> getMeshcopTxtAttributesWithVendorOui("AB.CD.EF"));
+    }
+
+    @Test
+    public void getMeshcopTxtAttributes_validVendorOui_accepted() {
+        assertThat(getMeshcopTxtAttributesWithVendorOui("010203")).isEqualTo(new byte[] {1, 2, 3});
+        assertThat(getMeshcopTxtAttributesWithVendorOui("01-02-03"))
+                .isEqualTo(new byte[] {1, 2, 3});
+        assertThat(getMeshcopTxtAttributesWithVendorOui("01:02:03"))
+                .isEqualTo(new byte[] {1, 2, 3});
+        assertThat(getMeshcopTxtAttributesWithVendorOui("ABCDEF"))
+                .isEqualTo(new byte[] {(byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
+        assertThat(getMeshcopTxtAttributesWithVendorOui("abcdef"))
+                .isEqualTo(new byte[] {(byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
+    }
+
+    private byte[] getMeshcopTxtAttributesWithVendorOui(String vendorOui) {
+        when(mResources.getString(eq(R.string.config_thread_vendor_oui))).thenReturn(vendorOui);
+        return ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources).vendorOui;
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_validTxts_returnsParsedTxtAttrs() {
+        String[] txts = new String[] {"va=123", "vb=", "vc"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs)
+                .containsExactly(
+                        new DnsTxtAttribute("va", "123".getBytes(UTF_8)),
+                        new DnsTxtAttribute("vb", new byte[] {}),
+                        new DnsTxtAttribute("vc", new byte[] {}));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtKeyNotStartWithV_throwsIllegalArgument() {
+        String[] txts = new String[] {"abc=123"};
+
+        assertThrows(
+                IllegalArgumentException.class, () -> mService.makeVendorSpecificTxtAttrs(txts));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtIsTooShort_throwsIllegalArgument() {
+        String[] txtEmptyKey = new String[] {"=123"};
+        String[] txtSingleCharKey = new String[] {"v=456"};
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mService.makeVendorSpecificTxtAttrs(txtEmptyKey));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mService.makeVendorSpecificTxtAttrs(txtSingleCharKey));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtValueIsEmpty_parseSuccess() {
+        String[] txts = new String[] {"va=", "vb"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs)
+                .containsExactly(
+                        new DnsTxtAttribute("va", new byte[] {}),
+                        new DnsTxtAttribute("vb", new byte[] {}));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_multipleEquals_splittedByTheFirstEqual() {
+        String[] txts = new String[] {"va=abc=def=123"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs).containsExactly(new DnsTxtAttribute("va", "abc=def=123".getBytes(UTF_8)));
+    }
+
+    @Test
     public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
         mService.initialize();
         final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
@@ -204,13 +425,13 @@
     }
 
     @Test
-    public void userRestriction_initWithUserRestricted_threadIsDisabled() {
+    public void userRestriction_initWithUserRestricted_otDaemonNotStarted() {
         when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
 
         mService.initialize();
         mTestLooper.dispatchAll();
 
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+        assertThat(mFakeOtDaemon.isInitialized()).isFalse();
     }
 
     @Test
@@ -225,15 +446,9 @@
 
     @Test
     public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
-        AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
         when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
-        doAnswer(
-                        invocation -> {
-                            receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
-                            return null;
-                        })
-                .when(mContext)
-                .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+        AtomicReference<BroadcastReceiver> receiverRef =
+                captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
         mService.initialize();
         mTestLooper.dispatchAll();
 
@@ -242,21 +457,14 @@
         mTestLooper.dispatchAll();
 
         assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
-        verify(mMockPersistentSettings, never())
-                .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+        assertThat(mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED)).isTrue();
     }
 
     @Test
-    public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
-        AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+    public void userRestriction_userBecomesNotRestricted_stateIsEnabled() {
         when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
-        doAnswer(
-                        invocation -> {
-                            receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
-                            return null;
-                        })
-                .when(mContext)
-                .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+        AtomicReference<BroadcastReceiver> receiverRef =
+                captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
         mService.initialize();
         mTestLooper.dispatchAll();
 
@@ -265,8 +473,6 @@
         mTestLooper.dispatchAll();
 
         assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
-        verify(mMockPersistentSettings, never())
-                .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
     }
 
     @Test
@@ -283,6 +489,24 @@
         assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
     }
 
+    private AtomicReference<BroadcastReceiver> captureBroadcastReceiver(String action) {
+        AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+
+        doAnswer(
+                        invocation -> {
+                            receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+                            return null;
+                        })
+                .when(mContext)
+                .registerReceiver(
+                        any(BroadcastReceiver.class),
+                        argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)),
+                        any(),
+                        any());
+
+        return receiverRef;
+    }
+
     private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
         return new IOperationReceiver.Stub() {
             @Override
@@ -298,6 +522,102 @@
     }
 
     @Test
+    public void
+            createRandomizedDataset_noNetworkTimeClock_datasetActiveTimestampIsNotAuthoritative()
+                    throws Exception {
+        MockitoSession session =
+                ExtendedMockito.mockitoSession().mockStatic(SystemClock.class).startMocking();
+        final IActiveOperationalDatasetReceiver mockReceiver =
+                ExtendedMockito.mock(IActiveOperationalDatasetReceiver.class);
+
+        try {
+            ExtendedMockito.when(SystemClock.currentNetworkTimeClock())
+                    .thenThrow(new DateTimeException("fake throw"));
+            mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+            mTestLooper.dispatchAll();
+        } finally {
+            session.finishMocking();
+        }
+
+        verify(mockReceiver, never()).onError(anyInt(), anyString());
+        verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+        ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+        assertThat(activeDataset.getActiveTimestamp().isAuthoritativeSource()).isFalse();
+    }
+
+    @Test
+    public void createRandomizedDataset_zeroNanoseconds_returnsZeroTicks() throws Exception {
+        Instant now = Instant.ofEpochSecond(0, 0);
+        Clock clock = Clock.fixed(now, ZoneId.systemDefault());
+        MockitoSession session =
+                ExtendedMockito.mockitoSession().mockStatic(SystemClock.class).startMocking();
+        final IActiveOperationalDatasetReceiver mockReceiver =
+                ExtendedMockito.mock(IActiveOperationalDatasetReceiver.class);
+
+        try {
+            ExtendedMockito.when(SystemClock.currentNetworkTimeClock()).thenReturn(clock);
+            mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+            mTestLooper.dispatchAll();
+        } finally {
+            session.finishMocking();
+        }
+
+        verify(mockReceiver, never()).onError(anyInt(), anyString());
+        verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+        ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+        assertThat(activeDataset.getActiveTimestamp().getTicks()).isEqualTo(0);
+    }
+
+    @Test
+    public void createRandomizedDataset_maxNanoseconds_returnsMaxTicks() throws Exception {
+        // The nanoseconds to ticks conversion is rounded in the current implementation.
+        // 32767.5 / 32768 * 1000000000 = 999984741.2109375, using 999984741 to
+        // produce the maximum ticks.
+        Instant now = Instant.ofEpochSecond(0, 999984741);
+        Clock clock = Clock.fixed(now, ZoneId.systemDefault());
+        MockitoSession session =
+                ExtendedMockito.mockitoSession().mockStatic(SystemClock.class).startMocking();
+        final IActiveOperationalDatasetReceiver mockReceiver =
+                ExtendedMockito.mock(IActiveOperationalDatasetReceiver.class);
+
+        try {
+            ExtendedMockito.when(SystemClock.currentNetworkTimeClock()).thenReturn(clock);
+            mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+            mTestLooper.dispatchAll();
+        } finally {
+            session.finishMocking();
+        }
+
+        verify(mockReceiver, never()).onError(anyInt(), anyString());
+        verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+        ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+        assertThat(activeDataset.getActiveTimestamp().getTicks()).isEqualTo(32767);
+    }
+
+    @Test
+    public void createRandomizedDataset_hasNetworkTimeClock_datasetActiveTimestampIsAuthoritative()
+            throws Exception {
+        MockitoSession session =
+                ExtendedMockito.mockitoSession().mockStatic(SystemClock.class).startMocking();
+        final IActiveOperationalDatasetReceiver mockReceiver =
+                ExtendedMockito.mock(IActiveOperationalDatasetReceiver.class);
+
+        try {
+            ExtendedMockito.when(SystemClock.currentNetworkTimeClock())
+                    .thenReturn(Clock.systemUTC());
+            mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+            mTestLooper.dispatchAll();
+        } finally {
+            session.finishMocking();
+        }
+
+        verify(mockReceiver, never()).onError(anyInt(), anyString());
+        verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+        ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+        assertThat(activeDataset.getActiveTimestamp().isAuthoritativeSource()).isTrue();
+    }
+
+    @Test
     public void createRandomizedDataset_succeed_activeDatasetCreated() throws Exception {
         final IActiveOperationalDatasetReceiver mockReceiver =
                 mock(IActiveOperationalDatasetReceiver.class);
@@ -332,4 +652,153 @@
         verify(mockReceiver, never()).onSuccess(any(ActiveOperationalDataset.class));
         verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
     }
+
+    @Test
+    public void forceStopOtDaemonForTest_noPermission_throwsSecurityException() {
+        doThrow(new SecurityException(""))
+                .when(mContext)
+                .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), any());
+
+        assertThrows(
+                SecurityException.class,
+                () -> mService.forceStopOtDaemonForTest(true, new IOperationReceiver.Default()));
+    }
+
+    @Test
+    public void forceStopOtDaemonForTest_enabled_otDaemonDiesAndJoinFails() throws Exception {
+        mService.initialize();
+        IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+        IOperationReceiver mockJoinReceiver = mock(IOperationReceiver.class);
+
+        mService.forceStopOtDaemonForTest(true, mockReceiver);
+        mTestLooper.dispatchAll();
+        mService.join(DEFAULT_ACTIVE_DATASET, mockJoinReceiver);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, times(1)).onSuccess();
+        assertThat(mFakeOtDaemon.isInitialized()).isFalse();
+        verify(mockJoinReceiver, times(1)).onError(eq(ERROR_THREAD_DISABLED), anyString());
+    }
+
+    @Test
+    public void forceStopOtDaemonForTest_disable_otDaemonRestartsAndJoinSccess() throws Exception {
+        mService.initialize();
+        IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+        IOperationReceiver mockJoinReceiver = mock(IOperationReceiver.class);
+
+        mService.forceStopOtDaemonForTest(true, mock(IOperationReceiver.class));
+        mTestLooper.dispatchAll();
+        mService.forceStopOtDaemonForTest(false, mockReceiver);
+        mTestLooper.dispatchAll();
+        mService.join(DEFAULT_ACTIVE_DATASET, mockJoinReceiver);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, times(1)).onSuccess();
+        assertThat(mFakeOtDaemon.isInitialized()).isTrue();
+        verify(mockJoinReceiver, times(1)).onSuccess();
+    }
+
+    @Test
+    public void onOtDaemonDied_joinedNetwork_interfaceStateBackToUp() throws Exception {
+        mService.initialize();
+        final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+        mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
+        mTestLooper.dispatchAll();
+
+        Mockito.reset(mMockInfraIfController);
+        mFakeOtDaemon.terminate();
+        mTestLooper.dispatchAll();
+
+        verify(mMockTunIfController, times(1)).onOtDaemonDied();
+        InOrder inOrder = Mockito.inOrder(mMockTunIfController);
+        inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(false);
+        inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(true);
+    }
+
+    @Test
+    public void setConfiguration_configurationUpdated() throws Exception {
+        mService.initialize();
+        final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+        final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+        final IOperationReceiver mockReceiver3 = mock(IOperationReceiver.class);
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcpv6PdEnabled(false)
+                        .build();
+        ThreadConfiguration config2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+        ThreadConfiguration config3 =
+                new ThreadConfiguration.Builder(config2).build(); // Same as config2
+
+        mService.setConfiguration(config1, mockReceiver1);
+        mService.setConfiguration(config2, mockReceiver2);
+        mService.setConfiguration(config3, mockReceiver3);
+        mTestLooper.dispatchAll();
+
+        assertThat(mPersistentSettings.getConfiguration()).isEqualTo(config3);
+        InOrder inOrder = Mockito.inOrder(mockReceiver1, mockReceiver2, mockReceiver3);
+        inOrder.verify(mockReceiver1).onSuccess();
+        inOrder.verify(mockReceiver2).onSuccess();
+        inOrder.verify(mockReceiver3).onSuccess();
+    }
+
+    @Test
+    public void initialize_upstreamNetworkRequestHasCertainTransportTypesAndCapabilities() {
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+                ArgumentCaptor.forClass(NetworkRequest.class);
+        verify(mMockConnectivityManager, atLeastOnce())
+                .registerNetworkCallback(
+                        networkRequestCaptor.capture(),
+                        any(ConnectivityManager.NetworkCallback.class),
+                        any(Handler.class));
+        List<NetworkRequest> upstreamNetworkRequests =
+                networkRequestCaptor.getAllValues().stream()
+                        .filter(nr -> !nr.hasTransport(TRANSPORT_THREAD))
+                        .toList();
+        assertThat(upstreamNetworkRequests.size()).isEqualTo(1);
+        NetworkRequest upstreamNetworkRequest = upstreamNetworkRequests.get(0);
+        assertThat(upstreamNetworkRequest.hasTransport(TRANSPORT_WIFI)).isTrue();
+        assertThat(upstreamNetworkRequest.hasTransport(TRANSPORT_ETHERNET)).isTrue();
+        assertThat(upstreamNetworkRequest.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+        assertThat(upstreamNetworkRequest.hasCapability(NET_CAPABILITY_INTERNET)).isTrue();
+    }
+
+    @Test
+    public void setTestNetworkAsUpstream_upstreamNetworkRequestAlwaysDisallowsVpn() {
+        mService.initialize();
+        mTestLooper.dispatchAll();
+        clearInvocations(mMockConnectivityManager);
+
+        final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+        final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+        mService.setTestNetworkAsUpstream("test-network", mockReceiver1);
+        mService.setTestNetworkAsUpstream(null, mockReceiver2);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<NetworkRequest> networkRequestCaptor =
+                ArgumentCaptor.forClass(NetworkRequest.class);
+        verify(mMockConnectivityManager, times(2))
+                .registerNetworkCallback(
+                        networkRequestCaptor.capture(),
+                        any(ConnectivityManager.NetworkCallback.class),
+                        any(Handler.class));
+        assertThat(networkRequestCaptor.getAllValues().size()).isEqualTo(2);
+        NetworkRequest networkRequest1 = networkRequestCaptor.getAllValues().get(0);
+        NetworkRequest networkRequest2 = networkRequestCaptor.getAllValues().get(1);
+        assertThat(networkRequest1.getNetworkSpecifier()).isNotNull();
+        assertThat(networkRequest1.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+        assertThat(networkRequest2.getNetworkSpecifier()).isNull();
+        assertThat(networkRequest2.hasCapability(NET_CAPABILITY_NOT_VPN)).isTrue();
+    }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
index 5ca6511..ca9741d 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -19,6 +19,7 @@
 import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
 
 import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -104,6 +105,7 @@
     @Mock List<SubscriptionInfo> mSubscriptionInfoList;
     @Mock SubscriptionInfo mSubscriptionInfo0;
     @Mock SubscriptionInfo mSubscriptionInfo1;
+    @Mock ThreadPersistentSettings mPersistentSettings;
 
     private ThreadNetworkCountryCode mThreadNetworkCountryCode;
     private boolean mErrorSetCountryCode;
@@ -164,7 +166,8 @@
                 mContext,
                 mTelephonyManager,
                 mSubscriptionManager,
-                oemCountryCode);
+                oemCountryCode,
+                mPersistentSettings);
     }
 
     private static Address newAddress(String countryCode) {
@@ -450,6 +453,14 @@
     }
 
     @Test
+    public void settingsCountryCode_settingsCountryCodeIsActive_settingsCountryCodeIsUsed() {
+        when(mPersistentSettings.get(THREAD_COUNTRY_CODE)).thenReturn(TEST_COUNTRY_CODE_CN);
+        mThreadNetworkCountryCode.initialize();
+
+        assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+    }
+
+    @Test
     public void dump_allCountryCodeInfoAreDumped() {
         StringWriter stringWriter = new StringWriter();
         PrintWriter printWriter = new PrintWriter(stringWriter);
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
index c7e0eca..dfb3129 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -16,17 +16,29 @@
 
 package com.android.server.thread;
 
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.validateMockitoUsage;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.PendingOperationalDataset;
 import android.os.Binder;
-import android.os.Process;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -34,6 +46,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -44,20 +57,44 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class ThreadNetworkShellCommandTest {
-    private static final String TAG = "ThreadNetworkShellCommandTTest";
-    @Mock ThreadNetworkService mThreadNetworkService;
-    @Mock ThreadNetworkCountryCode mThreadNetworkCountryCode;
-    @Mock PrintWriter mErrorWriter;
-    @Mock PrintWriter mOutputWriter;
+    // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+    // Active Timestamp: 1
+    // Channel: 19
+    // Channel Mask: 0x07FFF800
+    // Ext PAN ID: ACC214689BC40BDF
+    // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+    // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+    // Network Name: OpenThread-d9a0
+    // PAN ID: 0xD9A0
+    // PSKc: A245479C836D551B9CA557F7B9D351B4
+    // Security Policy: 672 onrcb
+    private static final String DEFAULT_ACTIVE_DATASET_TLVS =
+            "0E080000000000010000000300001335060004001FFFE002"
+                    + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+                    + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+                    + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+                    + "B9D351B40C0402A0FFF8";
 
-    ThreadNetworkShellCommand mThreadNetworkShellCommand;
+    @Mock private ThreadNetworkControllerService mControllerService;
+    @Mock private ThreadNetworkCountryCode mCountryCode;
+    @Mock private PrintWriter mErrorWriter;
+    @Mock private PrintWriter mOutputWriter;
+
+    private Context mContext;
+    private ThreadNetworkShellCommand mShellCommand;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        mThreadNetworkShellCommand = new ThreadNetworkShellCommand(mThreadNetworkCountryCode);
-        mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        doNothing()
+                .when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+
+        mShellCommand = new ThreadNetworkShellCommand(mContext, mControllerService, mCountryCode);
+        mShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
     }
 
     @After
@@ -66,11 +103,26 @@
     }
 
     @Test
-    public void getCountryCode_executeInUnrootedShell_allowed() {
-        BinderUtil.setUid(Process.SHELL_UID);
-        when(mThreadNetworkCountryCode.getCountryCode()).thenReturn("US");
+    public void getCountryCode_testingPermissionIsChecked() {
+        when(mCountryCode.getCountryCode()).thenReturn("US");
 
-        mThreadNetworkShellCommand.exec(
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"get-country-code"});
+
+        verify(mContext, times(1))
+                .enforceCallingOrSelfPermission(
+                        eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+    }
+
+    @Test
+    public void getCountryCode_currentCountryCodePrinted() {
+        when(mCountryCode.getCountryCode()).thenReturn("US");
+
+        mShellCommand.exec(
                 new Binder(),
                 new FileDescriptor(),
                 new FileDescriptor(),
@@ -81,60 +133,172 @@
     }
 
     @Test
-    public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
-        BinderUtil.setUid(Process.SHELL_UID);
-
-        mThreadNetworkShellCommand.exec(
+    public void forceSetCountryCodeEnabled_testingPermissionIsChecked() {
+        mShellCommand.exec(
                 new Binder(),
                 new FileDescriptor(),
                 new FileDescriptor(),
                 new FileDescriptor(),
                 new String[] {"force-country-code", "enabled", "US"});
 
-        verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(eq("US"));
-        verify(mErrorWriter).println(contains("force-country-code"));
+        verify(mContext, times(1))
+                .enforceCallingOrSelfPermission(
+                        eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
     }
 
     @Test
-    public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() {
-        BinderUtil.setUid(Process.ROOT_UID);
-
-        mThreadNetworkShellCommand.exec(
+    public void forceSetCountryCodeEnabled_countryCodeIsOverridden() {
+        mShellCommand.exec(
                 new Binder(),
                 new FileDescriptor(),
                 new FileDescriptor(),
                 new FileDescriptor(),
                 new String[] {"force-country-code", "enabled", "US"});
 
-        verify(mThreadNetworkCountryCode).setOverrideCountryCode(eq("US"));
+        verify(mCountryCode).setOverrideCountryCode(eq("US"));
     }
 
     @Test
-    public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
-        BinderUtil.setUid(Process.SHELL_UID);
-
-        mThreadNetworkShellCommand.exec(
+    public void forceSetCountryCodeDisabled_overriddenCountryCodeIsCleared() {
+        mShellCommand.exec(
                 new Binder(),
                 new FileDescriptor(),
                 new FileDescriptor(),
                 new FileDescriptor(),
                 new String[] {"force-country-code", "disabled"});
 
-        verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(any());
-        verify(mErrorWriter).println(contains("force-country-code"));
+        verify(mCountryCode).clearOverrideCountryCode();
     }
 
     @Test
-    public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
-        BinderUtil.setUid(Process.ROOT_UID);
-
-        mThreadNetworkShellCommand.exec(
+    public void forceStopOtDaemon_testingPermissionIsChecked() {
+        mShellCommand.exec(
                 new Binder(),
                 new FileDescriptor(),
                 new FileDescriptor(),
                 new FileDescriptor(),
-                new String[] {"force-country-code", "disabled"});
+                new String[] {"force-stop-ot-daemon", "enabled"});
 
-        verify(mThreadNetworkCountryCode).clearOverrideCountryCode();
+        verify(mContext, times(1))
+                .enforceCallingOrSelfPermission(
+                        eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+    }
+
+    @Test
+    public void forceStopOtDaemon_serviceThrows_failed() {
+        doThrow(new SecurityException(""))
+                .when(mControllerService)
+                .forceStopOtDaemonForTest(eq(true), any());
+
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-stop-ot-daemon", "enabled"});
+
+        verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
+        verify(mOutputWriter, never()).println();
+    }
+
+    @Test
+    public void forceStopOtDaemon_serviceApiTimeout_failedWithTimeoutError() {
+        doNothing().when(mControllerService).forceStopOtDaemonForTest(eq(true), any());
+
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"force-stop-ot-daemon", "enabled"});
+
+        verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
+        verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
+        verify(mOutputWriter, never()).println();
+    }
+
+    @Test
+    public void join_controllerServiceJoinIsCalled() {
+        doNothing().when(mControllerService).join(any(), any());
+
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"join", DEFAULT_ACTIVE_DATASET_TLVS});
+
+        var activeDataset =
+                ActiveOperationalDataset.fromThreadTlvs(
+                        base16().decode(DEFAULT_ACTIVE_DATASET_TLVS));
+        verify(mControllerService, times(1)).join(eq(activeDataset), any());
+        verify(mErrorWriter, never()).println();
+    }
+
+    @Test
+    public void join_invalidDataset_controllerServiceJoinIsNotCalled() {
+        doNothing().when(mControllerService).join(any(), any());
+
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"join", "000102"});
+
+        verify(mControllerService, never()).join(any(), any());
+        verify(mErrorWriter, times(1)).println(contains("Invalid dataset argument"));
+    }
+
+    @Test
+    public void migrate_controllerServiceMigrateIsCalled() {
+        doNothing().when(mControllerService).scheduleMigration(any(), any());
+
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"migrate", DEFAULT_ACTIVE_DATASET_TLVS, "300"});
+
+        ArgumentCaptor<PendingOperationalDataset> captor =
+                ArgumentCaptor.forClass(PendingOperationalDataset.class);
+        verify(mControllerService, times(1)).scheduleMigration(captor.capture(), any());
+        assertThat(captor.getValue().getActiveOperationalDataset())
+                .isEqualTo(
+                        ActiveOperationalDataset.fromThreadTlvs(
+                                base16().decode(DEFAULT_ACTIVE_DATASET_TLVS)));
+        assertThat(captor.getValue().getDelayTimer().toSeconds()).isEqualTo(300);
+        verify(mErrorWriter, never()).println();
+    }
+
+    @Test
+    public void migrate_invalidDataset_controllerServiceMigrateIsNotCalled() {
+        doNothing().when(mControllerService).scheduleMigration(any(), any());
+
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"migrate", "000102", "300"});
+
+        verify(mControllerService, never()).scheduleMigration(any(), any());
+        verify(mErrorWriter, times(1)).println(contains("Invalid dataset argument"));
+    }
+
+    @Test
+    public void leave_controllerServiceLeaveIsCalled() {
+        doNothing().when(mControllerService).leave(any());
+
+        mShellCommand.exec(
+                new Binder(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new FileDescriptor(),
+                new String[] {"leave"});
+
+        verify(mControllerService, times(1)).leave(any());
+        verify(mErrorWriter, never()).println();
     }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index 49b002a..ba489d9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -16,44 +16,53 @@
 
 package com.android.server.thread;
 
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
 import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED;
+
 import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.doAnswer;
+
 import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.validateMockitoUsage;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.res.Resources;
+import android.net.thread.ThreadConfiguration;
 import android.os.PersistableBundle;
 import android.util.AtomicFile;
+
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
+
 import com.android.connectivity.resources.R;
 import com.android.server.connectivity.ConnectivityResources;
-import java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
+
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+
 /** Unit tests for {@link ThreadPersistentSettings}. */
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class ThreadPersistentSettingsTest {
-    @Mock private AtomicFile mAtomicFile;
+    private static final String TEST_COUNTRY_CODE = "CN";
+
     @Mock Resources mResources;
     @Mock ConnectivityResources mConnectivityResources;
 
+    private AtomicFile mAtomicFile;
     private ThreadPersistentSettings mThreadPersistentSettings;
 
+    @Rule(order = 0)
+    public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -61,8 +70,7 @@
         when(mConnectivityResources.get()).thenReturn(mResources);
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
 
-        FileOutputStream fos = mock(FileOutputStream.class);
-        when(mAtomicFile.startWrite()).thenReturn(fos);
+        mAtomicFile = createAtomicFile();
         mThreadPersistentSettings =
                 new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
     }
@@ -76,7 +84,7 @@
     @Test
     public void initialize_readsFromFile() throws Exception {
         byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
-        setupAtomicFileMockForRead(data);
+        setupAtomicFileForRead(data);
 
         mThreadPersistentSettings.initialize();
 
@@ -86,7 +94,7 @@
     @Test
     public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
-        setupAtomicFileMockForRead(new byte[0]);
+        setupAtomicFileForRead(new byte[0]);
 
         mThreadPersistentSettings.initialize();
 
@@ -98,7 +106,7 @@
             throws Exception {
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
         byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
-        setupAtomicFileMockForRead(data);
+        setupAtomicFileForRead(data);
 
         mThreadPersistentSettings.initialize();
 
@@ -110,9 +118,6 @@
         mThreadPersistentSettings.put(THREAD_ENABLED.key, true);
 
         assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
-        // Confirm that file writes have been triggered.
-        verify(mAtomicFile).startWrite();
-        verify(mAtomicFile).finishWrite(any());
     }
 
     @Test
@@ -120,9 +125,81 @@
         mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
 
         assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
-        // Confirm that file writes have been triggered.
-        verify(mAtomicFile).startWrite();
-        verify(mAtomicFile).finishWrite(any());
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+    }
+
+    @Test
+    public void put_ThreadCountryCodeString_returnsString() throws Exception {
+        mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, TEST_COUNTRY_CODE);
+
+        assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
+    }
+
+    @Test
+    public void put_ThreadCountryCodeNull_returnsNull() throws Exception {
+        mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, null);
+
+        assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+    }
+
+    @Test
+    public void putConfiguration_sameValues_returnsFalse() {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+        mThreadPersistentSettings.putConfiguration(configuration);
+
+        assertThat(mThreadPersistentSettings.putConfiguration(configuration)).isFalse();
+    }
+
+    @Test
+    public void putConfiguration_differentValues_returnsTrue() {
+        ThreadConfiguration configuration1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcpv6PdEnabled(false)
+                        .build();
+        mThreadPersistentSettings.putConfiguration(configuration1);
+        ThreadConfiguration configuration2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcpv6PdEnabled(true)
+                        .build();
+
+        assertThat(mThreadPersistentSettings.putConfiguration(configuration2)).isTrue();
+    }
+
+    @Test
+    public void putConfiguration_nat64Enabled_valuesUpdatedAndPersisted() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+        mThreadPersistentSettings.putConfiguration(configuration);
+
+        assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+    }
+
+    @Test
+    public void putConfiguration_dhcpv6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setDhcpv6PdEnabled(true).build();
+        mThreadPersistentSettings.putConfiguration(configuration);
+
+        assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+    }
+
+    private AtomicFile createAtomicFile() throws Exception {
+        return new AtomicFile(mTemporaryFolder.newFile());
     }
 
     private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
@@ -133,19 +210,9 @@
         return outputStream.toByteArray();
     }
 
-    private void setupAtomicFileMockForRead(byte[] dataToRead) throws Exception {
-        FileInputStream is = mock(FileInputStream.class);
-        when(mAtomicFile.openRead()).thenReturn(is);
-        when(is.available()).thenReturn(dataToRead.length).thenReturn(0);
-        doAnswer(
-                        invocation -> {
-                            byte[] data = invocation.getArgument(0);
-                            int pos = invocation.getArgument(1);
-                            if (pos == dataToRead.length) return 0; // read complete.
-                            System.arraycopy(dataToRead, 0, data, 0, dataToRead.length);
-                            return dataToRead.length;
-                        })
-                .when(is)
-                .read(any(), anyInt(), anyInt());
+    private void setupAtomicFileForRead(byte[] dataToRead) throws Exception {
+        try (FileOutputStream outputStream = new FileOutputStream(mAtomicFile.getBaseFile())) {
+            outputStream.write(dataToRead);
+        }
     }
 }
diff --git a/thread/tests/utils/Android.bp b/thread/tests/utils/Android.bp
index 24e9bb9..726ec9d 100644
--- a/thread/tests/utils/Android.bp
+++ b/thread/tests/utils/Android.bp
@@ -27,6 +27,7 @@
         "net-tests-utils",
         "net-utils-device-common",
         "net-utils-device-common-bpf",
+        "net-utils-device-common-struct-base",
     ],
     srcs: [
         "src/**/*.java",
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
index 43f177d..b586a19 100644
--- a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -62,6 +62,7 @@
     private final Looper mLooper;
     private TestNetworkInterface mInterface;
     private TestableNetworkAgent mAgent;
+    private Network mNetwork;
     private final TestableNetworkCallback mNetworkCallback;
     private final ConnectivityManager mConnectivityManager;
 
@@ -91,6 +92,11 @@
         return mInterface.getInterfaceName();
     }
 
+    /** Returns the {@link android.net.Network} of the test network. */
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
     private void setUpTestNetwork() throws Exception {
         mInterface = mContext.getSystemService(TestNetworkManager.class).createTapInterface();
 
@@ -105,13 +111,13 @@
                         newNetworkCapabilities(),
                         lp,
                         new NetworkAgentConfig.Builder().build());
-        final Network network = mAgent.register();
+        mNetwork = mAgent.register();
         mAgent.markConnected();
 
         PollingCheck.check(
                 "No usable address on interface",
                 TIMEOUT.toMillis(),
-                () -> hasUsableAddress(network, getInterfaceName()));
+                () -> hasUsableAddress(mNetwork, getInterfaceName()));
 
         lp.setLinkAddresses(makeLinkAddresses());
         mAgent.sendLinkProperties(lp);
diff --git a/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java b/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
new file mode 100644
index 0000000..38a6e90
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.utils;
+
+import static com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A rule used to skip Thread tests when the device doesn't support a specific feature indicated by
+ * {@code ThreadFeatureCheckerRule.Requires*}.
+ */
+public final class ThreadFeatureCheckerRule implements TestRule {
+    private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
+    private static final int KERNEL_ANDROID_VERSION_MULTICAST_ROUTING_SUPPORTED = 14;
+
+    /**
+     * Annotates a test class or method requires the Thread feature to run.
+     *
+     * <p>In Absence of the Thread feature, the test class or method will be ignored.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    public @interface RequiresThreadFeature {}
+
+    /**
+     * Annotates a test class or method requires the kernel IPv6 multicast routing feature to run.
+     *
+     * <p>In Absence of the multicast routing feature, the test class or method will be ignored.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    public @interface RequiresIpv6MulticastRouting {}
+
+    /**
+     * Annotates a test class or method requires the simulation Thread device (i.e. ot-cli-ftd) to
+     * run.
+     *
+     * <p>In Absence of the simulation device, the test class or method will be ignored.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    public @interface RequiresSimulationThreadDevice {}
+
+    @Override
+    public Statement apply(final Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                if (hasAnnotation(RequiresThreadFeature.class, description)) {
+                    assumeTrue(
+                            "Skipping test because the Thread feature is unavailable",
+                            hasThreadFeature());
+                }
+
+                if (hasAnnotation(RequiresIpv6MulticastRouting.class, description)) {
+                    assumeTrue(
+                            "Skipping test because kernel IPv6 multicast routing is unavailable",
+                            hasIpv6MulticastRouting());
+                }
+
+                if (hasAnnotation(RequiresSimulationThreadDevice.class, description)) {
+                    assumeTrue(
+                            "Skipping test because simulation Thread device is unavailable",
+                            hasSimulationThreadDevice());
+                }
+
+                base.evaluate();
+            }
+        };
+    }
+
+    /** Returns {@code true} if a test method or the test class is annotated with annotation. */
+    private <T extends Annotation> boolean hasAnnotation(
+            Class<T> annotationClass, Description description) {
+        // Method annotation
+        boolean hasAnnotation = description.getAnnotation(annotationClass) != null;
+
+        // Class annotation
+        Class<?> clazz = description.getTestClass();
+        while (!hasAnnotation && clazz != Object.class) {
+            hasAnnotation |= clazz.getAnnotation(annotationClass) != null;
+            clazz = clazz.getSuperclass();
+        }
+
+        return hasAnnotation;
+    }
+
+    /** Returns {@code true} if this device has the Thread feature supported. */
+    private static boolean hasThreadFeature() {
+        final Context context = ApplicationProvider.getApplicationContext();
+
+        // Use service name rather than `ThreadNetworkManager.class` to avoid
+        // `ClassNotFoundException` on U- devices.
+        return context.getSystemService("thread_network") != null;
+    }
+
+    /**
+     * Returns {@code true} if this device has the kernel IPv6 multicast routing feature enabled.
+     */
+    private static boolean hasIpv6MulticastRouting() {
+        // The kernel IPv6 multicast routing (i.e. IPV6_MROUTE) is enabled on kernel version
+        // android14-5.15.0 and later
+        return isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED)
+                && isKernelAndroidVersionAtLeast(
+                        KERNEL_ANDROID_VERSION_MULTICAST_ROUTING_SUPPORTED);
+    }
+
+    /**
+     * Returns {@code true} if the android version in the kernel version of this device is equal to
+     * or larger than the given {@code minVersion}.
+     */
+    private static boolean isKernelAndroidVersionAtLeast(int minVersion) {
+        final String osRelease = VintfRuntimeInfo.getOsRelease();
+        final Pattern pattern = Pattern.compile("android(\\d+)");
+        Matcher matcher = pattern.matcher(osRelease);
+
+        if (matcher.find()) {
+            int version = Integer.parseInt(matcher.group(1));
+            return (version >= minVersion);
+        }
+        return false;
+    }
+
+    /** Returns {@code true} if the simulation Thread device is supported. */
+    private static boolean hasSimulationThreadDevice() {
+        // Simulation radio is supported on only Cuttlefish
+        return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
+    }
+}
diff --git a/tools/Android.bp b/tools/Android.bp
index 9216b5b..2c2ed14 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -83,6 +83,8 @@
     ],
     data: [
         "testdata/test-jarjar-excludes.txt",
+        // txt with Test classes to test they aren't included when added to jarjar excludes
+        "testdata/test-jarjar-excludes-testclass.txt",
         // two unsupportedappusage lists with different classes to test using multiple lists
         "testdata/test-unsupportedappusage.txt",
         "testdata/test-other-unsupportedappusage.txt",
diff --git a/tools/aospify_device.sh b/tools/aospify_device.sh
new file mode 100755
index 0000000..0176093
--- /dev/null
+++ b/tools/aospify_device.sh
@@ -0,0 +1,176 @@
+#!/bin/bash
+
+# Script to swap core networking modules in a GMS userdebug device to AOSP modules, by remounting
+# the system partition and replacing module prebuilts. This is only to be used for local testing,
+# and should only be used on userdebug devices that support "adb root" and remounting the system
+# partition using overlayfs. The setup wizard should be cleared before running the script.
+#
+# Usage: aospify_device.sh [device_serial]
+#
+# Reset with "adb enable-verity", then wiping data (from Settings, or:
+# "adb reboot bootloader && fastboot erase userdata && fastboot reboot").
+# Some devices output errors like "Overlayfs teardown failed" on "enable-verity" but it still works
+# (/mnt/scratch should be deleted).
+#
+# This applies to NetworkStack, CaptivePortalLogin, dnsresolver, tethering, cellbroadcast modules,
+# which generally need to be preloaded together (core networking modules + cellbroadcast which
+# shares its certificates with NetworkStack and CaptivePortalLogin)
+#
+# This allows device manufacturers to test their changes in AOSP modules, running them on their
+# own device builds, before contributing contributing the patches to AOSP. After running this script
+# once AOSP modules can be quickly built and updated on the prepared device with:
+#   m NetworkStack
+#   adb install --staged $ANDROID_PRODUCT_OUT/system/priv-app/NetworkStack/NetworkStack.apk \
+#   adb reboot
+# or for APEX modules:
+#   m com.android.tethering deapexer
+#   $ANDROID_HOST_OUT/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/com.android.tethering.capex --output /tmp/decompressed.apex
+#   adb install /tmp/decompressed.apex && adb reboot
+#
+# This has been tested on Android T and Android U Pixel devices. On recent (U+) devices, it requires
+# setting a released target SDK (for example target_sdk_version: "34") in
+# packages/modules/Connectivity/service/ServiceConnectivityResources/Android.bp before building.
+set -e
+
+function push_apex {
+    local original_apex_name=$1
+    local aosp_apex_name=$2
+    if $ADB_CMD shell ls /system/apex/$original_apex_name.capex 1>/dev/null 2>/dev/null; then
+        $ADB_CMD shell rm /system/apex/$original_apex_name.capex
+        $ADB_CMD push $ANDROID_PRODUCT_OUT/system/apex/$aosp_apex_name.capex /system/apex/
+    else
+        rm -f /tmp/decompressed_$aosp_apex_name.apex
+        $ANDROID_HOST_OUT/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/$aosp_apex_name.capex --output /tmp/decompressed_$aosp_apex_name.apex
+        if ! $ADB_CMD shell ls /system/apex/$original_apex_name.apex 1>/dev/null 2>/dev/null; then
+            # Filename observed on some phones, even though it is not actually compressed
+            original_apex_name=${original_apex_name}_compressed
+        fi
+        $ADB_CMD shell rm /system/apex/$original_apex_name.apex
+        $ADB_CMD push /tmp/decompressed_$aosp_apex_name.apex /system/apex/$aosp_apex_name.apex
+        rm /tmp/decompressed_$aosp_apex_name.apex
+    fi
+}
+
+function push_apk {
+    local app_type=$1
+    local original_apk_name=$2
+    local aosp_apk_name=$3
+    $ADB_CMD shell rm /system/$app_type/$original_apk_name/$original_apk_name*.apk
+    $ADB_CMD push $ANDROID_PRODUCT_OUT/system/$app_type/$aosp_apk_name/$aosp_apk_name.apk /system/$app_type/$original_apk_name/
+}
+
+NETWORKSTACK_AOSP_SEPOLICY_KEY="<signer signature=\"308205dc308203c4a003020102020900fc6cb0d8a6fdd16\
+8300d06092a864886f70d01010b0500308181310b30090603550406130255533113301106035504080c0a43616c69666f72\
+6e69613116301406035504070c0d4d6f756e7461696e20566965773110300e060355040a0c07416e64726f69643110300e0\
+60355040b0c07416e64726f69643121301f06035504030c18636f6d2e616e64726f69642e6e6574776f726b737461636b30\
+20170d3139303231323031343632305a180f34373537303130383031343632305a308181310b30090603550406130255533\
+113301106035504080c0a43616c69666f726e69613116301406035504070c0d4d6f756e7461696e20566965773110300e06\
+0355040a0c07416e64726f69643110300e060355040b0c07416e64726f69643121301f06035504030c18636f6d2e616e647\
+26f69642e6e6574776f726b737461636b30820222300d06092a864886f70d01010105000382020f003082020a0282020100\
+bb71f5137ff0b2d757acc2ca3d378e0f8de11090d5caf3d49e314d35c283b778b02d792d8eba440364ca970985441660f0b\
+c00afbc63dd611b1bf51ad28a1edd21e0048f548b80f8bd113e25682822f57dab8273afaf12c64d19a0c6be238f3e66ddc7\
+9b10fd926931e3ee60a7bf618644da3c2c4fc428139d45d27beda7fe45e30075b493ead6ec01cdd55d931c0a657e2e59742\
+ca632b6dc3842a2deb7d22443c809291d7a549203ae6ae356582a4ca23f30f0549c4ec8408a75278e95c69e8390ad5280bc\
+efaef6f1309a41bd9f3bfb5d12dca7e79ec6fd6848193fa9ab728224887b4f93e985ec7cbf6401b0e863a4b91c05d046f04\
+0fe954004b1645954fcb4114cee1e8b64b47d719a19ef4c001cb183f7f3e166e43f56d68047c3440da34fdf529d44274b8b\
+2f6afb345091ad8ad4b93bd5c55d52286a5d3c157465db8ddf62e7cdb6b10fb18888046afdd263ae6f2125d9065759c7e42\
+f8610a6746edbdc547d4301612eeec3c3cbd124dececc8d38b20e73b13f24ee7ca13a98c5f61f0c81b07d2b519749bc2bcb\
+9e0949aef6c118a3e8125e6ab57fce46bb091a66740e10b31c740b891900c0ecda9cc69ecb4f3369998b175106dd0a4ffd7\
+024eb7e75fedd1a5b131d0bb2b40c63491e3cf86b8957b21521b3a96ed1376a51a6ac697866b0256dee1bcd9ab9a188bf4c\
+ed80b59a5f24c2da9a55eb7b0e502116e30203010001a3533051301d0603551d0e041604149383c92cfbf099d5c47b0c365\
+7d8622a084b72e1301f0603551d230418301680149383c92cfbf099d5c47b0c3657d8622a084b72e1300f0603551d130101\
+ff040530030101ff300d06092a864886f70d01010b050003820201006a0501382fde2a6b8f70c60cd1b8ee4f788718c288b\
+170258ef3a96230b65005650d6a4c42a59a97b2ddec502413e7b438fbd060363d74b74a232382a7f77fd3da34e38f79fad0\
+35a8b472c5cff365818a0118d87fa1e31cc7ed4befd27628760c290980c3cc3b7ff0cfd01b75ff1fcc83e981b5b25a54d85\
+b68a80424ac26015fb3a4c754969a71174c0bc283f6c88191dced609e245f5938ffd0ad799198e2d0bf6342221c1b0a5d33\
+2ed2fffc668982cabbcb7d3b630ff8476e5c84ac0ad37adf9224035200039f95ec1fa95bf83796c0e8986135cee2dcaef19\
+0b249855a7e7397d4a0bf17ea63d978589c6b48118a381fffbd790c44d80233e2e35292a3b5533ca3f2cc173f85cf904adf\
+e2e4e2183dc1eba0ebae07b839a81ff1bc92e292550957c8599af21e9c0497b9234ce345f3f508b1cc872aa55ddb5e773c5\
+c7dd6577b9a8b6daed20ae1ff4b8206fd9f5c8f5a22ba1980bef01ae6fcb2659b97ad5b985fa81c019ffe008ddd9c8130c0\
+6fc6032b2149c2209fc438a7e8c3b20ce03650ad31c4ee48f169777a0ae182b72ca31b81540f61f167d8d7adf4f6bb2330f\
+f5c24037245000d8172c12ab5d5aa5890b8b12db0f0e7296264eb66e7f9714c31004649fb4b864005f9c43c80db3f6de52f\
+d44d6e2036bfe7f5807156ed5ab591d06fd6bb93ba4334ea2739af8b41ed2686454e60b666d10738bb7ba88001\">\
+<seinfo value=\"network_stack\"\/><\/signer>"
+
+DEVICE=$1
+ADB_CMD="adb -s $DEVICE"
+
+if [ -z "$DEVICE" ]; then
+    echo "Usage: aospify_device.sh [device_serial]"
+    exit 1
+fi
+
+if [ -z "$ANDROID_BUILD_TOP" ]; then
+    echo "Run build/envsetup.sh first to set ANDROID_BUILD_TOP"
+    exit 1
+fi
+
+if ! $ADB_CMD wait-for-device shell pm path com.google.android.networkstack 1>/dev/null 2>/dev/null; then
+    echo "This device is already not using GMS modules"
+    exit 1
+fi
+
+read -p "This script is only for test purposes and highly likely to make your device unusable. \
+Continue ? <y/N>" prompt
+if [[ $prompt != "y" ]]
+then
+    exit 0
+fi
+
+cd $ANDROID_BUILD_TOP
+source build/envsetup.sh
+lunch aosp_arm64-trunk_staging-userdebug
+m NetworkStack CaptivePortalLogin com.android.tethering com.android.cellbroadcast \
+    com.android.resolv deapexer \
+    out/target/product/generic_arm64/system/etc/selinux/plat_mac_permissions.xml \
+    out/target/product/generic_arm64/system/etc/permissions/com.android.networkstack.xml
+
+$ADB_CMD root
+$ADB_CMD remount
+$ADB_CMD reboot
+
+echo "Waiting for boot..."
+until [[ $($ADB_CMD wait-for-device shell getprop sys.boot_completed) == 1 ]]; do
+    sleep 1;
+done
+
+$ADB_CMD root
+$ADB_CMD remount
+
+push_apk priv-app NetworkStackGoogle NetworkStack
+push_apk app CaptivePortalLoginGoogle CaptivePortalLogin
+push_apex com.google.android.tethering com.android.tethering
+push_apex com.google.android.cellbroadcast com.android.cellbroadcast
+push_apex com.google.android.resolv com.android.resolv
+
+# Replace the network_stack key used to set its sepolicy context
+rm -f /tmp/pulled_plat_mac_permissions.xml
+$ADB_CMD pull /system/etc/selinux/plat_mac_permissions.xml /tmp/pulled_plat_mac_permissions.xml
+sed_replace='s/<signer signature="[0-9a-fA-F]+"><seinfo value="network_stack"\/><\/signer>/'$NETWORKSTACK_AOSP_SEPOLICY_KEY'/'
+sed -E "$sed_replace" /tmp/pulled_plat_mac_permissions.xml |
+    $ADB_CMD shell 'cat > /system/etc/selinux/plat_mac_permissions.xml'
+rm /tmp/pulled_plat_mac_permissions.xml
+
+# Update the networkstack privapp-permissions allowlist
+rm -f /tmp/pulled_privapp-permissions.xml
+networkstack_permissions=/system/etc/permissions/GoogleNetworkStack_permissions.xml
+if ! $ADB_CMD shell ls $networkstack_permissions 1>/dev/null 2>/dev/null; then
+    networkstack_permissions=/system/etc/permissions/privapp-permissions-google.xml
+fi
+
+$ADB_CMD pull $networkstack_permissions /tmp/pulled_privapp-permissions.xml
+
+# Remove last </permission> line, and the permissions for com.google.android.networkstack
+sed -nE '1,/<\/permissions>/p' /tmp/pulled_privapp-permissions.xml \
+    | sed -E '/com.google.android.networkstack/,/privapp-permissions/d' > /tmp/modified_privapp-permissions.xml
+# Add the AOSP permissions and re-add the </permissions> line
+sed -nE '/com.android.networkstack/,/privapp-permissions/p' $ANDROID_PRODUCT_OUT/system/etc/permissions/com.android.networkstack.xml \
+    >> /tmp/modified_privapp-permissions.xml
+echo '</permissions>' >> /tmp/modified_privapp-permissions.xml
+
+$ADB_CMD push /tmp/modified_privapp-permissions.xml $networkstack_permissions
+
+rm /tmp/pulled_privapp-permissions.xml /tmp/modified_privapp-permissions.xml
+
+echo "Done modifying, rebooting"
+$ADB_CMD reboot
\ No newline at end of file
diff --git a/tools/gen_jarjar_test.py b/tools/gen_jarjar_test.py
index f5bf499..12038e9 100644
--- a/tools/gen_jarjar_test.py
+++ b/tools/gen_jarjar_test.py
@@ -84,6 +84,31 @@
             'rule test.utils.TestUtilClass$TestInnerClassTest jarjar.prefix.@0\n',
             'rule test.utils.TestUtilClass$TestInnerClassTest$* jarjar.prefix.@0\n'], lines)
 
+    def test_gen_rules_repeated_testclass_excluded(self):
+        args = gen_jarjar.parse_arguments([
+            "jarjar-rules-generator-testjavalib.jar",
+            "--prefix", "jarjar.prefix",
+            "--output", "test-output-rules.txt",
+            "--apistubs", "framework-connectivity.stubs.module_lib.jar",
+            "--unsupportedapi", ":testdata/test-unsupportedappusage.txt",
+            "--excludes", "testdata/test-jarjar-excludes-testclass.txt",
+        ])
+        gen_jarjar.make_jarjar_rules(args)
+
+        with open(args.output) as out:
+            lines = out.readlines()
+
+        self.maxDiff = None
+        self.assertListEqual([
+            'rule android.net.IpSecTransform jarjar.prefix.@0\n',
+            'rule test.unsupportedappusage.OtherUnsupportedUsageClass jarjar.prefix.@0\n',
+            'rule test.unsupportedappusage.OtherUnsupportedUsageClassTest jarjar.prefix.@0\n',
+            'rule test.unsupportedappusage.OtherUnsupportedUsageClassTest$* jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClass jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClassTest jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClassTest$* jarjar.prefix.@0\n'], lines)
+
 
 if __name__ == '__main__':
     # Need verbosity=2 for the test results parser to find results
diff --git a/tools/testdata/test-jarjar-excludes-testclass.txt b/tools/testdata/test-jarjar-excludes-testclass.txt
new file mode 100644
index 0000000..f7cc2cb
--- /dev/null
+++ b/tools/testdata/test-jarjar-excludes-testclass.txt
@@ -0,0 +1,7 @@
+# Test file for excluded classes
+test\.jarj.rexcluded\.JarjarExcludedCla.s
+test\.jarjarexcluded\.JarjarExcludedClass\$TestInnerCl.ss
+
+# Exclude actual test files
+test\.utils\.TestUtilClassTest
+android\.net\.IpSecTransformTest
\ No newline at end of file